Files
khatibi/docs/visit_hold_entezar.md
2026-10-27 11:49:09 +03:30

745 lines
46 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# «قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد
این سند رفتار نهایی صفحهٔ `/panel/entezar/`، فیلد «تا تاریخ پیگیری نشود» در فرم
اقدام، و قواعد محاسبه را توضیح می‌دهد. هدف: بعد از این تغییر، **هماهنگیِ انجام‌شده
هم** مشتری را تا تاریخ توافق از فهرست خارج کند و کاربر بتواند بازدیدهای
«هماهنگ‌شده» را از «هماهنگ‌نشده» جدا ببیند.
---
## ۱. صورت مسئله
پیش از این تغییر، فقط دو حالت مشتری را از فهرست خارج می‌کرد:
1. اقدام `bazdid` با وضعیت `waite` و تاریخ آینده (بازدید برنامه‌ریزی‌شده)
2. اقدام `hamahangi` با تاریخ **آینده** در فیلد تاریخ اقدام
اما سناریوی واقعی کارشناس این بود:
> «همین حالا با مشتری تماس گرفتم، توافق کردیم هفتهٔ بعد بیاییم، و تیک *انجام شد* را
> زدم.»
در این حالت، فیلد «تاریخ اقدام» روی **همین حالا** می‌نشیند، پس هیچ قرار آینده‌ای
وجود نداشت و مشتری در فهرست می‌ماند و کارشناس دیگری دوباره تماس می‌گرفت.
**ریشه:** «تاریخ تماس» و «تاریخ قرار بعدی» دو مفهوم متفاوت‌اند ولی در یک فیلد
(`actionDateTime`) جمع شده بودند.
---
## ۲. راه‌حل: ستون «قرار بعدی»
دو ستون به مدل `Customer` اضافه شد:
| ستون | نوع | معنا |
|---|---|---|
| `visit_hold_until` | `DateTimeField(null=True)` | تا **پایان** این روز پیگیری لازم نیست |
| `visit_hold_set_at` | `DateTimeField(null=True)` | زمان ثبت، برای حسابرسی |
### قاعدهٔ طلایی: ستون، «مرجع» نیست
> `visit_hold_until` هیچ اعتباری به‌تنهایی ندارد. یک قرار دستی فقط وقتی اثر دارد
> که **هر سه** شرط برقرار باشد:
>
> ۱. تاریخش از حالا جلوتر باشد،
> ۲. برای همان مشتری یک اقدام **هماهنگیِ فعال** (`waite` یا `finish`) وجود داشته باشد،
> ۳. **آخرین اقدامِ جایگزین‌کنندهٔ قرار، همان هماهنگی باشد** — اگر بعد از آن
> «بازدید» (`bazdid`) یا «نصب» (`nasb`) ثبت شده باشد، قرار منتفی است.
> اقدام‌های دیگر (وصول، تماس ورودی/خروجی، …) قرار را باطل **نمی‌کنند**.
شرط ۳ برای این است که «ثبت بازدید پس از هماهنگی» قرار را خودکار باطل کند.
بدون آن، قرار قدیمی تا تاریخش زنده می‌ماند و مشتری در فهرست «با قرار» می‌ماند،
هرچند بازدید جای هماهنگی را گرفته است.
> **اصلاح ۲۰۲۶-۱۰-۲۲:** قبلاً «هر اقدام غیرهماهنگی» قرار را باطل می‌کرد؛ نتیجه
> این بود که یک سند وصول یا یک تماس ثبت‌شده، قرارِ توافق‌شده را بی‌صدا از بین
> می‌برد و مشتری دوباره در فهرست تماس ظاهر می‌شد. حالا فقط `bazdid`/`nasb`
> (یعنی اقدام‌هایی که واقعاً جای قرار می‌نشینند) قرار را باطل می‌کنند
> (`visit_policy._SUPERSEDING_TYPES`).
نتیجهٔ این قاعده:
| رویداد | اثر روی قرار |
|---|---|
| حذف اقدام هماهنگی | قرار **خودکار بی‌اثر** می‌شود |
| لغو هماهنگی (`cancell`) | بی‌اثر می‌شود |
| ویرایش تاریخ هماهنگی | قرار با تاریخ جدید هم‌راستا می‌شود |
| **ثبت بازدید یا نصب پس از هماهنگی** | قرار **خودکار بی‌اثر** می‌شود |
| ثبت سایر اقدام‌ها (وصول، تماس…) | قرار دست‌نخورده می‌ماند |
| کهنه ماندن ستون | **هیچ اثر مخربی ندارد** |
> **تکمیل ۲۰۲۶-۱۰-۲۷:** «نصب» در فهرست‌های کار هم مثل «بازدید» عمل می‌کند:
> `nasb + waite` با تاریخ از امروز به بعد و `nasb + finish` از موعد به بعد، ردیف را
> از `?view=today` و `?view=today&missed=1` خارج می‌کند
> (`visit_policy._pending_appointment_ids` و `cycle_anchor_map`). این دو نوع همان
> `_CYCLE_RESET_TYPES` هستند که چرخهٔ ۹ ماهه و قرار دستی را هم بازنشانی می‌کنند.
به همین دلیل دیگر خطر «کش کهنه» وجود ندارد: منبع حقیقت همیشه جدول `Action` است.
---
## ۳. سه منبع «قرار بعدی»
تصمیم «مشتری در فهرست هست یا نه» از اجتماع سه منبع گرفته می‌شود:
| منبع | تعریف | تاریخ قرار | مرز زمانی |
|---|---|---|---|
| `manual` | ستون `visit_hold_until` (با اعتبارسنجی بالا) | همان ستون (پایان روز) | پایان امروز |
| `scheduled` | `bazdid + waite` | `actionDateTime` | **پایان روز** |
| `coordination` | `hamahangi` با `waite` یا `finish` | `actionDateTime` (یا `finishDateTime`) | **لحظه‌ای** |
تابع واحد: `visit_policy.next_appointment_map(now)` — **همان تابعی که اسکنر پیامک
هم استفاده می‌کند**، پس صفحه و پیامک هرگز واگرا نمی‌شوند.
**چرا مرز زمانیِ دو منبع فرق دارد؟**
* در `scheduled`، `actionDateTime` یک **قرارِ آینده** است که کارشناس با تقویم
روزانه انتخاب می‌کند؛ پس «قرار امروز» باید تا پایان امروز معتبر بماند. با
مقایسهٔ لحظه‌ای، بازدیدِ امروز بلافاصله پس از گذشتن ساعتش از فهرست ناپدید
می‌شد (باگی که گزارش شد).
* در `coordination`، `actionDateTime` **زمانِ تماس** است، نه قرار آینده. اگر آن
را هم تا پایان روز معتبر بگیریم، هر تماسِ امروز مشتری را تا آخر امروز از
گروه «قرار دستی» بیرون می‌برد و سناریوی اصلی (تماس امروز + قرار دو هفته بعد)
بی‌اثر می‌شد.
اگر مشتری چند قرار داشته باشد، **زودترین** تاریخ ملاک نمایش است؛ اما برای
«بازگشت به فهرست» کافی است **آخرین** قرار منقضی شود.
---
## ۴. فرم ثبت اقدام (UI)
در مودال «ایجاد وظیفه» (`customerActionInc.html`):
| مورد | رفتار |
|---|---|
| نمایش فیلد | فقط وقتی نوع اقدام = **هماهنگی** انتخاب شود |
| برچسب | «تا تاریخ پیگیری نشود» |
| کنترل | تقویم شمسی (همان `pDatepicker` پروژه) |
| پیش‌فرض | اگر خالی بماند و تیک «انجام شد» زده شود ⇒ **+۱۴ روز** |
| سقف | **۶ ماه** (اعتبارسنجی سروری) |
| حالت ویرایش | فیلد با نوع اقدام هم‌راستا می‌شود (نمایش/پنهان) **و مقدار ذخیره‌شدهٔ مشتری در آن بارگذاری می‌شود** |
نکتهٔ مهم: فیلد «تاریخ و ساعتی که کار باید انجام شود» (`dateTimeAnjam`) همچنان
«زمان تماس» است و دست نخورده باقی می‌ماند. دو مفهوم، دو فیلد.
### ۴.۱ چیدمان مودال (mobile first)
مودال به سه گام شماره‌دار تقسیم شده تا کاربر بداند چه چیزی را کجا وارد کند:
| گام | محتوا |
|---|---|
| ۱ | نوع اقدام + مسئول (توسط) — کنار هم در دسکتاپ، زیر هم در موبایل |
| ۲ | زمان‌بندی: تاریخ انجام، «تا تاریخ پیگیری نشود»، و راهنمای بازشونده |
| ۳ | توضیحات، تیک «انجام شده»، تاریخ اتمام و نتیجه |
نکات پیاده‌سازی:
* پیشوند همهٔ کلاس‌ها `ax-` و همهٔ انتخابگرها زیر `#ActionBox` است ⇒ هیچ استایلی
به سایر صفحات نشت نمی‌کند.
* روی موبایل: کارت تمام‌عرض (`calc(100% - 1rem)`)، بدنهٔ اسکرول‌شونده، هدف لمسی
حداقل ۲٫۶rem، و دکمه‌های پاصفحه تمام‌عرض.
* روی دسکتاپ (≥۷۶۸px): شبکهٔ دوستونه، عرض حداکثر ۴۴rem.
* سرصفحه نام مشتری را نشان می‌دهد تا کاربر بداند برای چه کسی وظیفه می‌سازد.
* **همان تقویم شمسی قبلی** استفاده می‌شود: `dateTimePickerAnjam`،
`dateTimePickerFinish` و `dateTimePickerHoldUntil` با `pDatepicker` و
`calendar.persian.locale = 'fa'`. هیچ کتابخانهٔ جدیدی اضافه نشده است.
* **نام کلاس فیلد باید با سلکتور JS یکی باشد.** هر فیلد تاریخ دو المان دارد:
ورودی نمایشی (با کلاس `dateTimePickerXxx`) و ورودی مخفیِ ارسال (با کلاس
`dateTimePickerXxx-alt`). اگر این نام‌ها هم‌خوانی نداشته باشند،
`pDatepicker` روی مجموعهٔ خالی اجرا می‌شود، `undefined` برمی‌گرداند و
`setDate()` با `TypeError` کل هندلر را می‌شکند — نتیجه: تقویم باز نمی‌شود و
بقیهٔ منطق (پنهان‌کردن فیلدها، برداشتن تیک خودکار) هم اجرا نمی‌شود.
دو تست اختصاصی
(`test_every_picker_selector_matches_markup` و
`test_every_alt_field_class_exists_in_markup`) همین را قفل کرده‌اند.
* `altField` با فرمت پیش‌فرض `unix` مقدار می‌دهد؛ بنابراین `visitHoldUntil`
یک timestamp میلی‌ثانیه‌ای است که `_parse_form_datetime` آن را می‌خواند.
* تقویم به `body` اضافه می‌شود (نه داخل مودال) ⇒ اسکرول بدنهٔ مودال آن را نمی‌برد.
### ۴.۲ هشدار زمان‌بندی و پیش‌فرض «انجام شده» (فقط هماهنگی)
چون در عمل «ثبت هماهنگی» یعنی «همین حالا با مشتری تماس گرفتم»، انتخاب نوع
**هماهنگی** به‌طور خودکار تیک «این کار انجام شده است» را می‌زند (و فیلدهای
تاریخ اتمام و نتیجه را نمایش می‌دهد). برای جلوگیری از ثبتِ ناخواسته، دو لایه
محافظ اضافه شده است:
**۱) هشدار همیشگی** — به‌محض تیک‌خوردن و تا وقتی نوع هماهنگی است، یک کادر
کهربایی زیر تیک نمایش داده می‌شود:
> آیا مطمئن هستی که هماهنگی را انجام دادی؟
> اگر هنوز هماهنگی نکردی، این تیک را بردار.
**۲) تأیید در لحظهٔ ثبت** — اگر کاربر روی «ثبت» بزند و نوع هماهنگی و تیک
«انجام شده» فعال باشد، ارسال متوقف می‌شود و همان کادر برجسته شده و دو دکمه
نشان می‌دهد:
| دکمه | نتیجه |
|---|---|
| بله، هماهنگی انجام شد | فرم ارسال می‌شود (هماهنگی با وضعیت `finish` ثبت می‌شود) |
| نه، تیک را بردار | تیک برداشته می‌شود، فیلدهای اتمام پنهان می‌شوند و ارسال نمی‌شود |
قواعد ایمنی:
* **فقط هماهنگی**: سایر انواع اقدام (بازدید، نصب، وصول، تماس…) هیچ تیک
خودکار و هیچ تأییدی ندارند و رفتارشان دقیقاً مثل قبل است.
* **تغییر نوع = برداشتن تیک خودکار**: اگر سیستم تیک زده باشد و کاربر نوع را
به غیر هماهنگی تغییر دهد، تیک برداشته می‌شود (تا ناخواسته `last_bazdid_dt`
یا وضعیت «انجام شده» ثبت نشود). اگر کاربر خودش تیک زده باشد، دست‌نخورده
می‌ماند.
* **حالت ویرایش**: تأیید گرفته نمی‌شود (کاربر فقط رکورد موجود را اصلاح می‌کند)؛
اما کادر هشدار برای هماهنگیِ انجام‌شده به‌صورت اطلاعاتی نمایش داده می‌شود.
* **باز شدن مجدد مودال**: تیک، هشدار و وضعیت تأیید همیشه ریست می‌شوند.
#### بارگذاری مقدار ذخیره‌شده در حالت ویرایش
هر ردیف اقدام، مقدار فعلی `Customer.visit_hold_until` را به‌صورت ISO در
صفت `customer_hold` نگه می‌دارد. هنگام کلیک روی ردیف (باز کردن فرم ویرایش)،
تابع `seedHoldPicker()` آن را به تقویم شمسی و فیلد مخفی `visitHoldUntil`
می‌نویسد.
> **باگی که این بخش رفع می‌کند:** قبلاً فرم ویرایش مقدار را از دیتابیس
> نمی‌خواند. مقدار در سرور **درست ذخیره می‌شد** ولی چون فیلد هنگام باز شدن
> مجدد خالی نمایش داده می‌شد، کاربر فکر می‌کرد «ثبت نشده است».
ترتیب کار مهم است: `seedHoldPicker()` **بعد از** `toggleCoordinationField()`
صدا زده می‌شود، چون برای انواع غیرهماهنگی، `toggle` فیلد را پاک می‌کند.
#### ایمنی ویرایش رکوردهای قدیمی
در حالت **ویرایش**، تغییر نوع اقدام به غیرهماهنگی، قرار دستی را پاک
**نمی‌کند**؛ زیرا رکورد ویرایش‌شده ممکن است قدیمی‌تر از یک هماهنگیِ جدیدتر
باشد و پاک‌کردن، قرار معتبر را از بین می‌برد. اعتبارسنجی `manual_holds_map`
(شرط «آخرین فعالیت = هماهنگی») خودش قرار را بی‌اثر می‌کند.
در حالت **ایجاد** برعکس است: اقدام تازه آخرین فعالیت مشتری است، پس ثبت
بازدید جدید قرار قبلی را پاک می‌کند.
### ۴.۳ راهنمای بازشونده (فقط «هماهنگی»)
زیر فیلد تاریخ پیگیری، یک دکمهٔ راهنما با آیکون `!` قرار دارد که پنل توضیحات را
باز/بسته می‌کند. محتوای پنل:
* «این تاریخ چه کاری می‌کند؟» — تعریف ساده + اینکه تا پایان همان روز معتبر است.
* «مثال» — سناریوی واقعی با تاریخ نمونه.
* «نکات مهم» — خالی + تیک ⇒ ۱۴ روز، خالی + بدون تیک ⇒ بدون تعویق،
فقط تمدید (نه کوتاه‌کردن)، حذف/لغو هماهنگی ⇒ بازگشت فوری، سقف ۶ ماه،
تفاوت با «تاریخ بازدید پرسنل»، و توقف پیامک یادآوری در مدت تعویق.
رفتار:
| رویداد | نتیجه |
|---|---|
| انتخاب نوع = هماهنگی | دکمهٔ راهنما ظاهر می‌شود (پنل بسته) |
| کلیک روی دکمه | پنل باز/بسته می‌شود و `aria-expanded` به‌روز می‌شود |
| تغییر نوع به غیر هماهنگی | فیلد و راهنما پنهان و پنل بسته می‌شود |
| باز شدن مجدد مودال (ایجاد یا ویرایش) | پنل همیشه بسته شروع می‌شود |
دسترسی‌پذیری: دکمه `aria-expanded` و `aria-controls="holdHelpPanel"` دارد و
پنل با `is-hidden` کنترل می‌شود.
### قواعد اعتبارسنجی (سرور)
* تاریخ قرار نباید **پیش از امروز** باشد ⇒ پیام: «تاریخ قرار نمی‌تواند پیش از امروز باشد.»
* حداکثر **۱۸۳ روز** (≈۶ ماه) جلوتر ⇒ پیام: «قرار بعدی حداکثر می‌تواند ۱۸۳ روز (۶ ماه) جلوتر باشد.»
* تاریخ «امروز» پذیرفته است و به **۲۳:۵۹:۵۹ همان روز** تبدیل می‌شود (تا پایان روز معتبر).
* اگر اعتبارسنجی رد شود، **هیچ اقدامی ذخیره نمی‌شود**.
### قاعدهٔ «تمدید مجاز، کوتاه‌کردن ممنوع» (max)
قرار ذخیره‌شده همیشه **بزرگ‌ترین** تاریخِ ثبت‌شده است:
```
visit_hold_until = max(مقدار فعلی، تاریخ جدید)
```
| وضعیت | نتیجه |
|---|---|
| قرار فعلی ۱ آذر، کارشناس جدید ۸ آذر | **تمدید می‌شود به ۸ آذر** ✅ |
| قرار فعلی ۸ آذر، کارشناس جدید ۱ آذر | **تغییری نمی‌کند** (۸ آذر می‌ماند) |
منطق: اگر مشتری گفته «۸ آذر تماس بگیرید»، سیستم نباید ۱ آذر زنگ بزند. در مقابل،
یک ثبت اشتباه نباید قرارِ طولانیِ قبلی را نابود کند.
**برای عقب‌بردن قرار** (مثلاً مشتری گفت زودتر بیایید): هماهنگی را **حذف** و دوباره
با تاریخ جدید ثبت کنید.
---
## ۵. صفحهٔ انتظار: چهار گروه و فیلترها
تفکیک بر اساس **نوعِ منبع** است، نه «چند گروه ثابت». هر مشتری می‌تواند صفر تا
سه منبع داشته باشد:
| منبع | تعریف | فیلتر |
|---|---|---|
| `manual` | ستون `visit_hold_until` (تعویق از فرم هماهنگی) | `?view=manual` |
| `scheduled` | اقدام `bazdid` + `waite` با تاریخ آینده | `?view=scheduled` |
| `coordination` | اقدام `hamahangi` (waite/finish) با تاریخ آینده | `?view=coordination` |
> **باگ قبلی:** `coordination` با `scheduled` در یک گروه جمع می‌شد؛ نتیجه اینکه
> مشتریانی که فقط «هماهنگی» داشتند در نمای «بازدید برنامه‌ریزی‌شده» دیده
> می‌شدند و برچسب «بازدید برنامه‌ریزی‌شده» می‌گرفتند. حالا هر منبع فیلتر و
> ستون خودش را دارد.
### فیلترها (قابل بوکمارک و ترکیب با جست‌وجو)
| URL | نمایش | شامل |
|---|---|---|
| `/panel/entezar/` (بدون پارامتر) | **پیش‌فرض = نیازمند پیگیری امروز** | موعد تماس‌های امروز |
| `/panel/entezar/?view=today` | نیازمند پیگیری امروز (یا تاریخ `fdate`) | موعد تماس‌های آن روز |
| `?view=uncoordinated` | بدون قرار | بدون هیچ منبع |
| `?view=manual` | قرار دستی | هر مشتری با `manual` |
| `?view=scheduled` | بازدید برنامه‌ریزی‌شده | هر مشتری با `scheduled` |
| `?view=coordination` | هماهنگی | هر مشتری با `coordination` |
| `?view=coordinated` | همهٔ دارای قرار | اجتماع هر سه منبع |
| `?view=all` | همهٔ معوق‌ها | همه |
| مقدار نامعتبر | برگشت به پیش‌فرض (`today`)، **بدون خطا** | موعد تماس‌های امروز |
چون یک مشتری می‌تواند چند منبع داشته باشد، در چند فیلتر دیده می‌شود؛ اما در
`?view=coordinated` فقط **یک‌بار** شمرده می‌شود. به همین دلیل جمع سه کارت منبع
می‌تواند از «با قرار» بیشتر باشد (صفحه این را در راهنمای بالای خود توضیح می‌دهد).
### ستون‌های جدول
| ستون | توضیح |
|---|---|
| وضعیت قرار | «بدون قرار» / «قرار دستی» / «بازدید برنامه» / «هماهنگی» / «N قرار» |
| قرار دستی | تاریخ شمسی قرار دستی (یا —) |
| بازدید برنامه‌ریزی‌شده | تاریخ شمسی اقدام بازدید با تاریخ آینده (یا —) |
| هماهنگی | تاریخ شمسی اقدام هماهنگی با تاریخ آینده (یا —) |
| آخرین بازدید | بدون تغییر نسبت به قبل |
### شمارندهٔ داشبورد
`CacheTable('9mahbazdid')` = **بدون قرار** (کارِ فوری). هر بار باز شدن صفحه
به‌روز می‌شود. با تغییر تفکیک، این عدد **تغییر نمی‌کند**.
---
## ۶. تعامل با سایر بخش‌ها
| بخش | اثر |
|---|---|
| **آخرین بازدید** (`last_visit_map`) | دست‌نخورده — فقط `bazdid + finish` |
| **چرخهٔ ۹ ماهه** (`cycle_anchor_map`) | دست‌نخورده — `bazdid` و `nasb` |
| **اسکنر پیامک** (`scan_visit`) | فقط یک منبع اضافه در `next_appointment_map` |
| **`recalc_last_bazdid`** | دست‌نخورده |
| **`last_bazdid_dt`** | بدون تغییر (نمایش/ترتیب) |
| **ایندکس `act_visit_idx`** | بدون تغییر |
قاعدهٔ طلایی حفظ شده است: **هماهنگی هرگز بازدید حساب نمی‌شود.**
---
## ۶.۱ حذف یک اقدام (soft-delete)
دکمهٔ «حذف» فقط در صفحهٔ اقدام‌های مشتری (`/panel/customer/<id>/action`) نمایش
داده می‌شود و **هیچ رکوردی فیزیکی حذف نمی‌شود**؛ به‌جای آن:
| ستون | مقدار |
|---|---|
| `status` | `cancell` |
| `deleted_at` | زمان حذف |
| `deleted_by` | کاربر حذف‌کننده |
### چرا `cancell` و نه ستون `is_deleted`؟
همهٔ محاسبات (`last_visit_map`، `cycle_anchor_map`، `appointment_sources_map`،
`followups_on_date`، `overdue_followups`) از قبل فقط `waite`/`finish` را
می‌بینند؛ و `ActionList*` و `ActionCount` هم با `status='waite'` فیلتر می‌شوند.
پس همین یک مقدار، اقدام را از **همهٔ** خروجی‌ها خارج می‌کند — بدون Manager
سفارشی، بدون تغییر ایندکس پوشا و بدون دست‌زدن به ۳۱ کوئری.
### قاعدهٔ محصولی: اقدام دارای نظرسنجی حذف نمی‌شود
اگر `nazarDT` پر باشد (نظرسنجی ثبت‌شده)، حذف انجام **نمی‌شود**؛ چون هیچ
آرشیو/کپی‌ای وجود ندارد و گزارش رضایت‌سنجی برای همیشه تغییر می‌کرد. در UI
دکمه به حالت قفل‌شده با برچسب «نظرسنجی» درمی‌آید.
### پیامدهای جانبی (در همان تراکنش)
۱. `last_bazdid_dt` بازمحاسبه می‌شود (کش کهنه نمی‌ماند).
۲. قرار دستی اگر دیگر مرجعی نداشته باشد، خودکار بی‌اثر می‌شود.
۳. پیامک‌های **ارسال‌نشدهٔ** «بازدید ثبت شد» همان مشتری به `cancelled` می‌روند
(پیامک‌های ارسال‌شده دست نمی‌خورند).
### اثر روی مصرف‌کننده‌ها
| بخش | اثر |
|---|---|
| `/panel/entezar/` (همهٔ نماها) | اقدام کاملاً نادیده گرفته می‌شود |
| `/panel/ActionList/<u>/today` | از صف وظایف خارج می‌شود |
| `ActionCount` داشبورد | بلافاصله کم می‌شود |
| فهرست اقدام‌های مشتری | به‌طور پیش‌فرض پنهان (با `?status=cancell` قابل مشاهده برای حسابرسی) |
| `/panel/customer/<id>/nazar/` | پنهان |
| `/panel/ActionList/all` | پنهان |
| گزارش وصول (`finance.py`) | آخرین اقدام وصول دیگر آن را نمی‌بیند |
| داشبورد رضایت‌سنجی و گزارش روزانه | `deleted_at__isnull=True` |
| صف پیامک | `CANCELLED` در `DONE_STATUSES` ⇒ از صف بیرون می‌رود |
### امنیت
- فقط **POST** مجاز است؛ درخواست GET ریدایرکت می‌شود و چیزی را عوض نمی‌کند.
- `get_object_or_404(Action, id=..., customer=customer)` ⇒ حذف اقدام مشتری دیگر
ممکن نیست (۴۰۴).
- توکن CSRF در فرم اجباری است.
- تأیید دو‌مرحله‌ای پیش از حذف (نوع اقدام در متن تأیید نمایش داده می‌شود).
---
## ۷. سناریوهای تست‌شده
| # | سناریو | نتیجهٔ انتظار | وضعیت |
|---|---|---|---|
| ۱ | هماهنگی با قرار «امروز» | تا پایان روز خارج از فهرست | ✅ |
| ۲ | هماهنگی finish بدون تاریخ قرار | پیش‌فرض ۱۴ روز | ✅ |
| ۳ | حذف اقدام هماهنگی | قرار بی‌اثر، مشتری برمی‌گردد | ✅ |
| ۴ | لغو هماهنگی (`cancell`) | قرار بی‌اثر | ✅ |
| ۵ | هماهنگی دوم با تاریخ کوچک‌تر | قرار قبلی حفظ می‌شود | ✅ |
| ۶ | هماهنگی دوم با تاریخ بزرگ‌تر | قرار تمدید می‌شود | ✅ |
| ۷ | قرار بیش از ۶ ماه | رد با پیام، هیچ ذخیره‌ای | ✅ |
| ۸ | قرار در گذشته | رد با پیام | ✅ |
| ۹ | بازدید واقعی بعد از قرار | مشتری از فهرست خارج می‌ماند | ✅ |
| ۱۰ | هماهنگی finish با تاریخ آینده (بدون ستون) | خارج از فهرست | ✅ |
| ۱۱ | تفکیک A/B/C/D و هر پنج فیلتر | اعداد و فهرست درست | ✅ |
| ۱۲ | `?view=nonsense` | پیش‌فرض، بدون خطا | ✅ |
| ۱۳ | اسکنر پیامک با قرار دستی | پیامک ارسال نمی‌شود | ✅ |
| ۱۴ | اجتماع منابع در `next_appointment_map` | زودترین تاریخ | ✅ |
جمع: **۳۷۶ تست موفق** در کل پروژه (۱۵ تست جدید مربوط به این قابلیت).
---
## ۸. استقرار
```bash
# ۱) بکاپ
python manage.py sqlite_db_backup # یا کپی فایل دیتابیس
# ۲) مهاجرت (دو ستون nullable — امن و سریع)
python manage.py migrate
# ۳) بررسی
python manage.py check
python manage.py test tests
```
### رفتار دادهٔ موجود
* همهٔ مشتریان با `visit_hold_until = NULL` شروع می‌کنند ⇒ **رفتار امروز دقیقاً حفظ می‌شود**.
* هماهنگی‌های قدیمی (۵ رکورد سال ۲۰۲۲) هیچ ستونی ندارند و فقط از مسیر `Action`
ارزیابی می‌شوند.
---
## ۹. نکات عملیاتی
1. **حذف هماهنگی** بهترین راه برای «لغو قرار» است.
2. اگر قرار اشتباه ثبت شد، هماهنگی را **حذف** و دوباره ثبت کنید (قرار جدید فقط
می‌تواند تمدید کند، نه آن را کوتاه‌تر کند).
---
# بخش دوم — امکانات جدید رابط کاربری صفحهٔ انتظار
این بخش قابلیت‌هایی را توضیح می‌دهد که برای «راحت دیدن اطلاعات و تصمیم درست
گرفتن» اضافه شده‌اند. طراحی **mobile first** است: در موبایل هر مشتری یک کارت
می‌شود و در دسکتاپ همان جدول قبلی؛ هیچ صفحه/منوی جدیدی اضافه نشده و شکل کلی
صفحه حفظ شده است.
## ۱. نوار آمار (بالای صفحه)
چهار کارت. عنوان‌ها با **زبان کسب‌وکار** انتخاب شده‌اند، نه زبان دیتابیس. هر کارت
هم عدد را نشان می‌دهد و هم **لینک فیلتر** است:
| کارت | معنا | توضیح زیر عدد |
|---|---|---|
| **نیازمند پیگیری امروز** | موعد تماسشان **امروز** است و بازدید ندارند — فهرست کارِ امروز | موعد تماسشان امروز است — بازدید ندارند |
| **همهٔ مشتریان عقب‌افتاده** | همه، با هر وضعیت پیگیری | با هر وضعیت پیگیری |
| **تعیین تکلیف نشده** | هنوز هیچ وقت بازدید/پیگیری ندارند | هنوز وقت بازدید تعیین نشده — نیاز به تماس |
| **تعیین تکلیف شده** | وقت بعدی‌شان مشخص است | وقت بعدی‌شان مشخص است |
> **چرا «مشتری» و نه «بازدید»؟** عددِ کارت دوم تعداد **مشتری** است، نه تعداد
> بازدید. یک مشتری می‌تواند ده‌ها بازدید گذشته داشته باشد ولی فقط **یک بار**
> در این فهرست شمرده شود.
### تفکیک نوع قرار — در دراپ‌داون، نه روی کارت
کارت‌ها **عمداً ساده** نگه داشته شده‌اند: فقط عنوان، عدد و یک توضیح. جزئیات سه
نوع قرار (تعویق / بازدید / پیگیری) روی کارت چاپ **نمی‌شود**؛ چون:
- شش عدد روی کارت‌ها باعث سردرگمی می‌شد (بازخورد کاربران).
- ارتفاع کارت‌ها نامساوی و چیدمان موبایل به‌هم می‌ریخت.
- همان اطلاعات از دراپ‌داون **«نوع قرار»** در نوار فیلتر در دسترس است.
> جمع سه منبع می‌تواند از عدد «تعیین تکلیف شده» بیشتر باشد، چون یک مشتری
> می‌تواند چند منبع قرار داشته باشد؛ در `?view=coordinated` هر مشتری فقط
> **یک‌بار** شمرده می‌شود.
## ۱.۱ نمای «نیازمند پیگیری امروز» (`?view=today`) — **پیش‌فرض صفحه**
> **پیش‌فرض:** با باز کردن `/panel/entezar/` (بدون پارامتر)، همین نما نمایش داده
> می‌شود؛ کاربر بلافاصله فهرست کارِ امروز را می‌بیند.
فهرست کاریِ روزانهٔ کارشناس: **با چه کسانی امروز باید تماس بگیریم؟**
### این فهرست از کجا می‌آید؟
دو منبع، هر دو با شرط «موعد = همان روز»:
| منبع | تعریف |
|---|---|
| **هماهنگی** | اقدام `hamahangi` با `status` در (`waite`/`finish`) و `actionDateTime` (وگرنه `finishDateTime`) در همان روز |
| **قرار دستی** | `Customer.visit_hold_until` که تاریخش همان روز است (با همان اعتبارسنجی «ستون، مرجع نیست») |
### قاعدهٔ قطعی: «بازدید» اینجا نمی‌آید
> اگر برای مشتری **قرار بازدید** (`bazdid + waite`) ثبت شده باشد، **هرگز** در این
> فهرست نمایش داده نمی‌شود — حتی اگر برای همان روز هم موعد پیگیری داشته باشد.
دلیلش روشن است: وقتی پرسنل برای بازدید می‌آید، تماس تلفنی لازم نیست.
**استثنای مهم:** بازدیدهای بسیار قدیمی که هرگز بسته نشده‌اند (backlog) قرار فعال
محسوب نمی‌شوند؛ وگرنه یک رکورد کهنه می‌توانست مشتری را برای همیشه از فهرست
پنهان کند. شرط دقیق: بازدیدِ در انتظار با تاریخ **≥ آغاز روز مورد نظر**.
### تفاوت با فهرست «عقب‌افتاده»
| | «نیازمند پیگیری» | «عقب‌افتاده» |
|---|---|---|
| شرط ورود | موعد تماس = آن روز | از آخرین بازدید بیش از دوره گذشته |
| مشتری سالم | ✅ اگر قرار تماس داشته باشد می‌آید | ❌ نمی‌آید |
| شرط `installDate` | ندارد | دارد |
| ترتیب پیش‌فرض | زودترین ساعت موعد | قدیمی‌ترین بازدید |
### تقویم شمسی (گذشته و آینده)
نوار تاریخ فقط در این نما ظاهر می‌شود و شامل این‌هاست:
- **تقویم شمسی** (`pDatepicker`) برای انتخاب هر روزی
- دکمه‌های **‹ روز قبل** و **روز بعد ›**
- دکمهٔ **بازگشت به امروز** (اگر تاریخ انتخابی امروز نباشد)
```
/panel/entezar/?view=today امروز
/panel/entezar/?view=today&fdate=1405/07/28 روز دیگر (شمسی)
```
- مقدار نامعتبر/خالی ⇒ **امروز** (بدون خطا).
- عدد کارت «نیازمند پیگیری امروز» **همیشه عدد امروز** را نشان می‌دهد، حتی وقتی
در تقویم روز دیگری را می‌بینید.
- فیلتر «میزان تأخیر» و تفکیک آن در این نما **پنهان** می‌شود (مشتری این فهرست
ممکن است اصلاً تأخیری نداشته باشد)؛ اما جست‌وجو و مرتب‌سازی فعال است.
### ستون «تأخیر» در این نما نمایش داده نمی‌شود
این فهرست دربارهٔ «موعد تماس امروز» است، نه عقب‌افتادگی بازدید؛ پس ستون «تأخیر»
فقط در سایر نماها دیده می‌شود. عرض آن (۸٪) به ستون «نام» اضافه می‌شود تا مجموع
عرض ستون‌ها همچنان دقیقاً ۱۰۰٪ بماند (۸ ستون به‌جای ۹).
### هشدار «پیگیری‌های عقب‌افتاده»
اگر موعد پیگیری مشتریانی گذشته باشد و هنوز تعیین تکلیف نشده باشند، زیر نوار تاریخ
یک هشدار مینیمال قرمز با نقطهٔ کوچک نمایش داده می‌شود:
```
● 72 مورد نیازمند پیگیری از روزهای قبل باقی مانده
```
قواعد محاسبه (`visit_policy.overdue_followups`):
- مبنای هر مشتری، **آخرین** اقدام هماهنگی (`waite`/`finish`) است.
- اگر آن تاریخ **پیش از امروز** باشد ⇒ عقب‌افتاده.
- اگر مشتری هماهنگیِ امروز یا آینده داشته باشد ⇒ عقب‌افتاده **نیست**
(یعنی تمدید شده و از فهرست بیرون می‌رود).
- اگر برایش **قرار بازدید** ثبت شده باشد ⇒ حذف می‌شود (همان قاعدهٔ «بازدید گذاشته
شده ⇒ تماس لازم نیست»).
- هماهنگیِ لغوشده (`cancell`) اصلاً دیده نمی‌شود.
کلیک روی هشدار، همان فهرست را باز می‌کند:
```
/panel/entezar/?view=today&missed=1
```
در این حالت یک چیپ قرمز «نمایش N پیگیریِ عقب‌افتاده ✕ بازگشت به امروز» نشان داده
می‌شود و هشدار پنهان می‌گردد. عدد هشدار **دقیقاً** برابر تعداد ردیف‌های همین
فهرست است (هر دو از یک مسیر محاسبه می‌شوند).
## ۲. فیلترها (نوار زیر کارت‌ها)
یک ردیف فشرده با چهار کنترل؛ همه با هم ترکیب‌پذیرند و در URL حفظ می‌شوند.
| کنترل | پارامتر | مقادیر |
|---|---|---|
| نوع قرار | `view` | `all` · `uncoordinated` · `coordinated` · `manual` · `scheduled` · `coordination` |
| میزان تأخیر | `delay` | `lt30` · `30to90` · `90to180` · `gt180` (خالی = همه) |
| مرتب‌سازی | `sort` + `dir` | `last_visit` · `overdue` · `name` · `status` · `coordination` (و هر ستون دیگر) |
| جست‌وجو | `searchVal` + `searchOn` | `name` · `phone` · `code` · `address` |
- تغییر هر دراپ‌داون ⇒ فرم **خودکار** ارسال می‌شود.
- دکمهٔ **✕ حذف فیلترها** فقط وقتی فیلتری فعال است ظاهر می‌شود و همه را به
حالت پیش‌فرض برمی‌گرداند.
- **کارت‌ها هیچ‌وقت با فیلتر تغییر نمی‌کنند** — همیشه کل جامعه را نشان می‌دهند.
فقط فهرست زیرشان فیلتر می‌شود.
## ۳. تفکیک مدت تأخیر (کلیک‌پذیر)
زیر نوار فیلتر، تعداد مشتریان بر اساس **مدت عقب‌افتادگی** در **نمای جاری**
نمایش داده می‌شود و هر عدد **لینک فیلتر** است:
- تا ۳۰ روز
- ۳۰ تا ۹۰ روز
- ۹۰ تا ۱۸۰ روز
- بیش از ۶ ماه
**مبنای محاسبه:** «موعد بازدید» = آخرین بازدید معتبر + دورهٔ قالب. برای
مشتریِ بدون بازدید، مبنای مقایسه مقطع دوره است تا عدد بی‌معنا نشود.
یعنی «۳ ماه تأخیر» = «۳ ماه از موعد بازدید گذشته»، نه «۳ ماه از خود بازدید».
> جمع این چهار عدد همیشه برابر تعداد ردیف‌های نمای جاری است (تست‌شده).
## ۴. مرتب‌سازی (ستون‌ها و دراپ‌داون)
**هر ۹ ستون** با کلیک روی سرستون مرتب می‌شوند؛ کلیک دوباره جهت را برمی‌گرداند:
| ستون | مبنا |
|---|---|
| نام و نام خانوادگی | الفبایی |
| وضعیت قرار | بدون قرار ← تعویق ← بازدید ← پیگیری ← چندقراره |
| قرار دستی | تاریخ قرار (خالی‌ها آخر) |
| بازدید برنامه‌ریزی‌شده | تاریخ قرار (خالی‌ها آخر) |
| هماهنگی | تاریخ پیگیری (خالی‌ها آخر) |
| آخرین بازدید | تاریخ مؤثر بازدید از `Action` |
| تأخیر | تعداد روز تأخیر |
| تلفن | مقدار `phone1` |
| آدرس | الفبایی |
- سرستون فعال با فلش **▲/▼** و ستون‌های دیگر با **⇅** مشخص می‌شوند.
- **مقادیر خالی همیشه آخر می‌آیند**، مستقل از جهت — وگرنه مرتب‌سازی ستون‌های
قرار، فهرست را بی‌معنا می‌کرد.
- دراپ‌داون «مرتب‌سازی» چند گزینهٔ پرکاربرد را کوتاه می‌کند.
- مقادیر قدیمی `sort=oldest|newest|overdue` همچنان کار می‌کنند (سازگاری با
بوکمارک‌های قبلی).
## ۵. جست‌وجو
| فیلد | جست‌وجو در |
|---|---|
| نام و نام خانوادگی | `name_family` |
| تلفن | `phone1`، `phone2`، `phone3` |
| آدرس | `address` |
| کد مشتری | `idCode` |
نما، فیلتر تأخیر و مرتب‌سازی هنگام جست‌وجو **حفظ می‌شوند**.
## ۵. چیدمان ستون‌ها (بازطراحی)
پیش‌تر ستون «ردیف» یک ستون کامل جدول را برای یک عدد دو رقمی می‌گرفت و دو ستون
با `min-width` ثابت (۱۹۰px و ۳۲۰px) تعیین عرض می‌کردند؛ نتیجه: بی‌تناسبی
فاصله‌ها و اسکرول افقی. تغییرات:
1. **ستون «ردیف» حذف شد.** شمارهٔ ردیف حالا یک **چیپ کوچک کنار نام** است
(`.ez-idx`) که هم فضا نمی‌خورد و هم برای پیدا کردن ردیف کافی است.
2. **عرض ستون‌ها با `<colgroup>` درصدی** تعیین می‌شود و جدول
`table-layout: fixed` است. بنابراین:
- مجموع عرض‌ها همیشه ۱۰۰٪ است،
- هیچ ستونی بی‌دلیل پهن نمی‌شود،
- اسکرول افقی در حالت جدول رخ نمی‌دهد.
3. **مقیاس فاصله یکسان:** همهٔ حاشیه‌ها مضرب ۰٫۲۵rem (۴px) هستند.
4. **سرستون چسبان (sticky)** هنگام اسکرول عمودی.
5. **اعداد هم‌عرض** (`tabular-nums`) تا ستون تاریخ‌ها لق نزند.
6. **تلفن با `direction: ltr`** تا ارقام در متن راست‌به‌چپ جابه‌جا نشوند.
7. **آدرس حداکثر دو خط** (`line-clamp`) با متن کامل در `title`.
8. **تأخیر به‌شکل چیپ رنگی** (خاکستری/کهربایی/قرمز/تیره) برای اسکن سریع.
| ستون | عرض | محتوا |
|---|---|---|
| نام و نام خانوادگی | ۲۳٪ | چیپ شماره + نام + کد مشتری |
| وضعیت قرار | ۱۳٪ | برچسب رنگی وضعیت |
| قرار هماهنگی | ۱۰٪ | تاریخ شمسی قرار دستی |
| بازدید برنامه‌ریزی‌شده | ۱۱٪ | تاریخ قرار از `Action` |
| آخرین بازدید | ۱۰٪ | تاریخ مؤثر واقعی |
| تأخیر | ۹٪ | چیپ تعداد روز |
| تلفن | ۱۱٪ | شمارهٔ اول (LTR) |
| آدرس | ۱۳٪ | دو خط + `title` |
**نقطهٔ شکست (breakpoint):** تا **۸۹۹px** حالت «کارت» و از **۹۰۰px** حالت
«جدول». این دو مرز باید همیشه یکی باشند؛ اگر نباشند، در بازه‌ای از عرض صفحه هر
دو حالت هم‌زمان اعمال می‌شوند و چیدمان می‌شکند. یک تست اختصاصی
(`test_card_and_table_breakpoints_agree`) همین را قفل کرده است.
## ۶. قابلیت‌های موبایل
- هر ردیف یک **کارت** می‌شود. نام مشتری سرِ کارت است (بدون برچسب) و بقیهٔ
مقادیر با برچسب ثابت ۶٫۵rem هم‌تراز می‌شوند.
- برچسب «وضعیت» حذف می‌شود چون خودِ برچسب رنگی گویاست.
- دکمهٔ **«تماس»** (`tel:`) و **«ثبت اقدام»** به‌صورت دو دکمهٔ تمام‌عرض.
- شماره‌های دوم و سوم در موبایل مخفی‌اند (برای تماس، شمارهٔ اول کافی است).
## ۷. نمای خالی
اگر در نما/جست‌وجوی جاری موردی نباشد، پیام «موردی در این نما پیدا نشد»
نمایش داده می‌شود (به‌جای جدولِ خالی و گمراه‌کننده).
## ۸. کارایی
| سنجه | مقدار |
|---|---|
| تعداد کوئری در بارگذاری صفحه | **۱۹** |
| رندر کامل صفحه روی دادهٔ واقعی (۳٬۲۴۳ معوق) | ~۱ ثانیه |
نکتهٔ پیاده‌سازی: دورهٔ قالب **یک بار** خوانده می‌شود و به حلقهٔ محاسبهٔ تأخیر
پاس داده می‌شود (`months=period_months`). اگر این پارامتر پاس نشود، برای هر
مشتری یک کوئری اضافه زده می‌شود (باگ N+1 که در تست‌ها کشف و رفع شد).
## ۹. آنچه تغییر نکرد
- منطق «چه کسی عقب‌افتاده است» (همان `last_visit_map` + `due_cut`).
- قاعدهٔ اعتبار قرار دستی (وجود هماهنگی فعال + آخرین فعالیت بودن).
- `visit_policy.py` به‌طور کامل: `next_appointment_map`، `scheduled_visits_map`،
`coordination_holds_map` — پس **اسکنر پیامک**، `recalc_last_bazdid` و چرخهٔ
۹ ماهه دقیقاً مثل قبل کار می‌کنند.
- شمارندهٔ داشبورد (`CacheTable('9mahbazdid')` = بدون قرار).
- لینک‌های قدیمی `?view=...` و رفتار پیش‌فرض (نمای «بدون قرار»).
## ۱۰. پارامترهای URL
```
/panel/entezar/ پیش‌فرض: نیازمند پیگیری امروز
?view=all همهٔ عقب‌افتاده‌ها
?view=coordinated تعیین تکلیف شده (اجتماع سه منبع)
?view=today نیازمند پیگیری امروز
?view=today&fdate=1405/07/28 نیازمند پیگیری در یک روز دیگر (شمسی)
?view=manual فقط تعویق دستی
?view=scheduled فقط بازدید برنامه‌ریزی‌شده
?view=coordination فقط پیگیری تلفنی
?delay=gt180 فقط بیش از ۶ ماه تأخیر
?sort=overdue&dir=desc بیشترین تأخیر اول
?sort=name&dir=asc الفبایی (صعودی)
?sort=manual&dir=desc تاریخ قرار دستی (نزولی)
?searchVal=رضایی&searchOn=name جست‌وجوی نام
?searchVal=0912&searchOn=phone جست‌وجوی تلفن
?view=all&delay=gt180&sort=name&dir=asc&page=3 همه با هم (ترکیب‌پذیر)
?view=today&missed=1 فقط پیگیری‌های عقب‌افتاده
```
- **اندازهٔ صفحه = ۱۰۰ ردیف** (`ENTEZAR_PAGE_SIZE`)؛ صفحه‌بندی فقط از ردیف ۱۰۱ به
بعد فعال می‌شود.
- مقدار نامعتبر `view`/`sort`/`delay` بی‌صدا به پیش‌فرض برمی‌گردد (بدون ۴۰۰/۵۰۰).
- مقادیر قدیمی `sort=oldest|newest|overdue` همچنان معتبرند.
- همهٔ لینک‌های داخلی (کارت‌ها، بازه‌های تأخیر، سرستون‌ها، صفحه‌بندی) **بقیهٔ
فیلترها را حفظ می‌کنند**.
---
3. پس از تاریخ قرار، مشتری **خودکار** در گروه «تعیین تکلیف نشده» ظاهر می‌شود؛
نیازی به کار دستی نیست.
4. عدد «تعیین تکلیف نشده» در داشبورد همان لیست کاریِ فوری است.