# «قرار بعدی» بازدید و صفحهٔ انتظار — توضیح کامل عملکرد این سند رفتار نهایی صفحهٔ `/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)` — **همان تابعی که اسکنر پیامک هم استفاده میکند**، پس صفحه و پیامک هرگز واگرا نمیشوند. **چرا مرز زمانیِ دو منبع فرق دارد؟** * در `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` یا وضعیت «انجام شده» ثبت نشود). اگر کاربر خودش تیک زده باشد، دستنخورده میماند. * **حالت ویرایش**: تأیید گرفته نمیشود (کاربر فقط رکورد موجود را اصلاح میکند)؛ اما کادر هشدار برای هماهنگیِ انجامشده بهصورت اطلاعاتی نمایش داده میشود. * **باز شدن مجدد مودال**: تیک، هشدار و وضعیت تأیید همیشه ریست میشوند. ### ۴.۳ راهنمای بازشونده (فقط «هماهنگی») زیر فیلد تاریخ پیگیری، یک دکمهٔ راهنما با آیکون `!` قرار دارد که پنل توضیحات را باز/بسته میکند. محتوای پنل: * «این تاریخ چه کاری میکند؟» — تعریف ساده + اینکه تا پایان همان روز معتبر است. * «مثال» — سناریوی واقعی با تاریخ نمونه. * «نکات مهم» — خالی + تیک ⇒ ۱۴ روز، خالی + بدون تیک ⇒ بدون تعویق، فقط تمدید (نه کوتاهکردن)، حذف/لغو هماهنگی ⇒ بازگشت فوری، سقف ۶ ماه، تفاوت با «تاریخ بازدید پرسنل»، و توقف پیامک یادآوری در مدت تعویق. رفتار: | رویداد | نتیجه | |---|---| | انتخاب نوع = هماهنگی | دکمهٔ راهنما ظاهر میشود (پنل بسته) | | کلیک روی دکمه | پنل باز/بسته میشود و `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. **عرض ستونها با `