# «قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد این سند رفتار نهایی صفحهٔ `/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`) وجود داشته باشد. نتیجهٔ این قاعده: | رویداد | اثر روی قرار | |---|---| | حذف اقدام هماهنگی | قرار **خودکار بیاثر** میشود | | لغو هماهنگی (`cancell`) | بیاثر میشود | | ویرایش تاریخ هماهنگی | قرار با تاریخ جدید همراستا میشود | | کهنه ماندن ستون | **هیچ اثر مخربی ندارد** | به همین دلیل دیگر خطر «کش کهنه» وجود ندارد: منبع حقیقت همیشه جدول `Action` است. --- ## ۳. سه منبع «قرار بعدی» تصمیم «مشتری در فهرست هست یا نه» از اجتماع سه منبع گرفته میشود: | منبع | تعریف | تاریخ قرار | |---|---|---| | `manual` | ستون `visit_hold_until` (با اعتبارسنجی بالا) | همان ستون (پایان روز) | | `scheduled` | `bazdid + waite` با تاریخ آینده | `actionDateTime` | | `coordination` | `hamahangi` با `waite` یا `finish` و تاریخ آینده | `actionDateTime` (یا `finishDateTime`) | تابع واحد: `visit_policy.next_appointment_map(now)` — **همان تابعی که اسکنر پیامک هم استفاده میکند**، پس صفحه و پیامک هرگز واگرا نمیشوند. اگر مشتری چند قرار داشته باشد، **زودترین** تاریخ ملاک نمایش است؛ اما برای «بازگشت به فهرست» کافی است **آخرین** قرار منقضی شود. --- ## ۴. فرم ثبت اقدام (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` یا وضعیت «انجام شده» ثبت نشود). اگر کاربر خودش تیک زده باشد، دستنخورده میماند. * **حالت ویرایش**: تأیید گرفته نمیشود (کاربر فقط رکورد موجود را اصلاح میکند)؛ اما کادر هشدار برای هماهنگیِ انجامشده بهصورت اطلاعاتی نمایش داده میشود. * **باز شدن مجدد مودال**: تیک، هشدار و وضعیت تأیید همیشه ریست میشوند. ### ۴.۳ راهنمای بازشونده (فقط «هماهنگی») زیر فیلد تاریخ پیگیری، یک دکمهٔ راهنما با آیکون `!` قرار دارد که پنل توضیحات را باز/بسته میکند. محتوای پنل: * «این تاریخ چه کاری میکند؟» — تعریف ساده + اینکه تا پایان همان روز معتبر است. * «مثال» — سناریوی واقعی با تاریخ نمونه. * «نکات مهم» — خالی + تیک ⇒ ۱۴ روز، خالی + بدون تیک ⇒ بدون تعویق، فقط تمدید (نه کوتاهکردن)، حذف/لغو هماهنگی ⇒ بازگشت فوری، سقف ۶ ماه، تفاوت با «تاریخ بازدید پرسنل»، و توقف پیامک یادآوری در مدت تعویق. رفتار: | رویداد | نتیجه | |---|---| | انتخاب نوع = هماهنگی | دکمهٔ راهنما ظاهر میشود (پنل بسته) | | کلیک روی دکمه | پنل باز/بسته میشود و `aria-expanded` بهروز میشود | | تغییر نوع به غیر هماهنگی | فیلد و راهنما پنهان و پنل بسته میشود | | باز شدن مجدد مودال (ایجاد یا ویرایش) | پنل همیشه بسته شروع میشود | دسترسیپذیری: دکمه `aria-expanded` و `aria-controls="holdHelpPanel"` دارد و پنل با `is-hidden` کنترل میشود. ### قواعد اعتبارسنجی (سرور) * تاریخ قرار نباید **پیش از امروز** باشد ⇒ پیام: «تاریخ قرار نمیتواند پیش از امروز باشد.» * حداکثر **۱۸۳ روز** (≈۶ ماه) جلوتر ⇒ پیام: «قرار بعدی حداکثر میتواند ۱۸۳ روز (۶ ماه) جلوتر باشد.» * تاریخ «امروز» پذیرفته است و به **۲۳:۵۹:۵۹ همان روز** تبدیل میشود (تا پایان روز معتبر). * اگر اعتبارسنجی رد شود، **هیچ اقدامی ذخیره نمیشود**. ### قاعدهٔ «تمدید مجاز، کوتاهکردن ممنوع» (max) قرار ذخیرهشده همیشه **بزرگترین** تاریخِ ثبتشده است: ``` visit_hold_until = max(مقدار فعلی، تاریخ جدید) ``` | وضعیت | نتیجه | |---|---| | قرار فعلی ۱ آذر، کارشناس جدید ۸ آذر | **تمدید میشود به ۸ آذر** ✅ | | قرار فعلی ۸ آذر، کارشناس جدید ۱ آذر | **تغییری نمیکند** (۸ آذر میماند) | منطق: اگر مشتری گفته «۸ آذر تماس بگیرید»، سیستم نباید ۱ آذر زنگ بزند. در مقابل، یک ثبت اشتباه نباید قرارِ طولانیِ قبلی را نابود کند. **برای عقببردن قرار** (مثلاً مشتری گفت زودتر بیایید): هماهنگی را **حذف** و دوباره با تاریخ جدید ثبت کنید. --- ## ۵. صفحهٔ انتظار: چهار گروه و فیلترها هر مشتری معوق **دقیقاً در یکی** از این چهار گروه است: | گروه | تعریف | |---|---| | **A** | معوق، بدون هیچ قرار | | **B** | معوق، فقط قرار دستی (`manual`) | | **C** | معوق، فقط بازدید برنامهریزیشده یا هماهنگی در انتظار | | **D** | معوق، هم قرار دستی و هم بازدید برنامهریزیشده | ### فیلترها (قابل بوکمارک و ترکیب با جستوجو) | URL | نمایش | شامل | |---|---|---| | `/panel/entezar/` (بدون پارامتر) | **پیشفرض = فقط A** (رفتار قبلی صفحه) | A | | `?view=uncoordinated` | بدون قرار | A | | `?view=manual` | قرار هماهنگی | B + D | | `?view=scheduled` | بازدید برنامهریزیشده | C + D | | `?view=coordinated` | همهٔ دارای قرار | B + C + D | | `?view=all` | همهٔ معوقها | A + B + C + D | | مقدار نامعتبر | برگشت به پیشفرض، **بدون خطا** | A | ### ستونهای جدول | ستون | توضیح | |---|---| | وضعیت قرار | برچسب «با قرار» / «بدون قرار» | | قرار هماهنگی | تاریخ شمسی قرار دستی (یا —) | | بازدید برنامهریزیشده | تاریخ شمسی قرار `Action` (یا —) | | آخرین بازدید | بدون تغییر نسبت به قبل | ### شمارندهٔ داشبورد `CacheTable('9mahbazdid')` = **گروه A** (کارِ فوری = بدون قرار). هر بار باز شدن صفحه بهروز میشود. --- ## ۶. تعامل با سایر بخشها | بخش | اثر | |---|---| | **آخرین بازدید** (`last_visit_map`) | دستنخورده — فقط `bazdid + finish` | | **چرخهٔ ۹ ماهه** (`cycle_anchor_map`) | دستنخورده — `bazdid` و `nasb` | | **اسکنر پیامک** (`scan_visit`) | فقط یک منبع اضافه در `next_appointment_map` | | **`recalc_last_bazdid`** | دستنخورده | | **`last_bazdid_dt`** | بدون تغییر (نمایش/ترتیب) | | **ایندکس `act_visit_idx`** | بدون تغییر | قاعدهٔ طلایی حفظ شده است: **هماهنگی هرگز بازدید حساب نمیشود.** --- ## ۷. سناریوهای تستشده | # | سناریو | نتیجهٔ انتظار | وضعیت | |---|---|---|---| | ۱ | هماهنگی با قرار «امروز» | تا پایان روز خارج از فهرست | ✅ | | ۲ | هماهنگی 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** است: در موبایل هر مشتری یک کارت میشود و در دسکتاپ همان جدول قبلی؛ هیچ صفحه/منوی جدیدی اضافه نشده و شکل کلی صفحه حفظ شده است. ## ۱. نوار آمار (بالای صفحه) پنج کارت که هر کدام هم **عدد** را نشان میدهد و هم **لینک فیلتر** است: | کارت | چه چیزی را نشان میدهد | |---|---| | کل معوق | همهٔ مشتریان عقبافتاده (با قرار و بدون قرار) | | بدون قرار | کسانی که هیچ قراری ندارند — **کار عملیاتی امروز** | | با قرار | قرار هماهنگی یا بازدید برنامهریزیشده دارند | | قرار هماهنگی | قرار دستی ثبتشده روی مشتری | | بازدید برنامهریزیشده | بازدید/هماهنگیِ در جریان از نوع `Action` | کارت فعال با حاشیهٔ آبی مشخص میشود. ## ۲. تفکیک مدت تأخیر زیر نوار آمار، تعداد مشتریان بر اساس **مدت عقبافتادگی** نمایش داده میشود: - کمتر از ۳۰ روز - ۳۰ تا ۹۰ روز - ۹۰ تا ۱۸۰ روز - بیش از ۶ ماه **مبنای محاسبه:** «موعد بازدید» = آخرین بازدید معتبر + دورهٔ قالب (پیشفرض ۹ ماه). برای مشتریِ بدون بازدید، مبنای مقایسه مقطع دوره است تا عدد بیمعنا نشود. یعنی «۳ ماه تأخیر» یعنی «۳ ماه از موعد بازدید گذشته»، نه «۳ ماه از خود بازدید». ## ۳. مرتبسازی سه حالت اختیاری (پارامتر `sort` در URL، قابل بوکمارک): | مقدار | ترتیب | کاربرد | |---|---|---| | `oldest` (پیشفرض) | قدیمیترین بازدید اول | اولویت کارشناس | | `overdue` | بیشترین تأخیر اول | ببینیم چه کسی از همه دیرتر است | | `newest` | جدیدترین بازدید اول | بررسی موارد تازهمعوق | ترتیب بر مبنای **تاریخ مؤثر بازدید از `Action`** است، نه ستون `last_bazdid_dt` (که ممکن است کهنه باشد). برای گروهِ بدون بازدید، مبنای ترتیب مقطع دوره است. ## ۴. جستوجو (تکمیلشده) پیشتر فقط جستوجو بر اساس **آدرس** ممکن بود. اکنون: | فیلد | جستوجو در | |---|---| | نام و نام خانوادگی | `name_family` | | تلفن | `phone1`، `phone2`، `phone3` | | آدرس | `address` | | کد مشتری | `idCode` | نما و مرتبسازی هنگام جستوجو **حفظ میشوند** و دکمهٔ «حذف جستجو» برای برگشت به فهرست کامل هست. ## ۵. چیدمان ستونها (بازطراحی) پیشتر ستون «ردیف» یک ستون کامل جدول را برای یک عدد دو رقمی میگرفت و دو ستون با `min-width` ثابت (۱۹۰px و ۳۲۰px) تعیین عرض میکردند؛ نتیجه: بیتناسبی فاصلهها و اسکرول افقی. تغییرات: 1. **ستون «ردیف» حذف شد.** شمارهٔ ردیف حالا یک **چیپ کوچک کنار نام** است (`.ez-idx`) که هم فضا نمیخورد و هم برای پیدا کردن ردیف کافی است. 2. **عرض ستونها با `