diff --git a/design-prototypes/chaharsu/README.md b/design-prototypes/chaharsu/README.md new file mode 100644 index 0000000..92a30a1 --- /dev/null +++ b/design-prototypes/chaharsu/README.md @@ -0,0 +1,166 @@ +# چهارسو — نمونهٔ مستقل با تمرکز بر خدمات + +فایل `index.html` را مستقیم در Chrome یا Edge باز کنید. اجرای Django، ورود به حساب و اتصال به دیتابیس لازم نیست. + +## راهنمای طراحی‌های بعدی + +[راهنمای سبک طراحی چهارسو](STYLE_GUIDE.md) شامل توکن‌ها، تایپوگرافی، فاصله‌ها، آیکون‌ها، اجزا، مثال‌ها و موارد نیازمند یکدست‌سازی است. برای طراحی جدید این سند را مبنا قرار دهید؛ توالی تصمیم‌های قدیمی README جایگزین آن نیست. + +## نسخهٔ اصلاح‌شده + +صفحهٔ اول فقط برای شناخت و انتخاب خدمات است: +- عنوان کوتاه «خدمات چهارسو». +- سه کارت خدمات: «ارتباط با کارشناسان»، «ارسال فیش واریزی» و «شماره حساب‌ها». کارت ارتباط با کارشناسان دو اقدام مستقل و مشخص دارد: ثبت درخواست جدید و مشاهده درخواست‌های من. +- هیچ فهرست درخواست، آمار درخواست‌ها، بخش «پیگیری‌ها» یا یادآوری تکمیل پروفایل در خانه نمایش داده نمی‌شود. +- بنر بزرگ و خوشامدگویی جدا حذف شده‌اند تا خدمات بالاتر دیده شوند. +- نوار پایین به سه مقصد «خدمات، درخواست‌ها، پروفایل» محدود شده؛ خانه و خدمات یک مقصد هستند. +- «درخواست‌های من» نمای جدا در همین نمونه دارد؛ انتخاب کارت یا منو آن را باز می‌کند. بازگشت به خدمات و دکمه‌های Back/Forward مرورگر کار می‌کنند. +- وضعیت خدمات مالی آینده با «به‌زودی» مشخص است؛ کلیک روی آن‌ها به دو فایل HTML مستقل می‌رود؛ هیچ پنجرهٔ modal برای خدمات مالی باز نمی‌شود. + +نام چهارسو محدودیتی برای تعداد خدمات ایجاد نمی‌کند. خدمات بعدی می‌توانند به شبکهٔ کارت‌ها اضافه شوند؛ برای هر خدمت جدید نیازی به افزودن گزینه به نوار پایین نیست. + +## فایل‌های بازبینی + +- `index.html`: نمونهٔ تعاملی خانه +- `payment-receipt.html`: صفحهٔ مستقل ارسال فیش واریزی +- `bank-accounts.html`: صفحهٔ مستقل شماره حساب‌ها +- `service-pages.css`: سبک‌های مشترک دو صفحهٔ خدمات مالی +- `receipt-mobile.png` و `accounts-mobile.png`: تصاویر موبایل صفحات مالی +- `mobile.png`: صفحهٔ نخست موبایل +- `desktop.png`: صفحهٔ نخست دسکتاپ +- `requests-mobile.png`: نمای مستقل درخواست‌ها + +نمای جدا در نمونه با `index.html#requests` نیز باز می‌شود. این یک مسیر اجرایی جدید در Django نیست. + +## مبنای بررسی معماری + +در مرحلهٔ نخست، Graphify برای معماری و Ripwire برای تطبیق با سورس فعلی استفاده شد. گراف موجود مربوط به گزارش ۲۰۲۶/۱۰/۰۴ و commit `39e64854` بود؛ تصمیم‌های طراحی با سورس فعلی تطبیق داده شدند. گراف بازسازی یا تغییر داده نشد. + +- پروژه Django Modular Monolith است. حساب‌ها، مشتریان، کارکنان، درخواست‌ها، پیام‌ها، پیوست‌ها، اعلان‌ها و core مسئولیت‌های جدا دارند. +- مسیر `/tickets/` به `customer_list` می‌رسد و مخصوص مشتری است. `customer_tickets` درخواست‌های همان مشتری را برمی‌گرداند. +- صفحه فعلی از `templates/base.html` ارث می‌برد. برند و متن‌های معرفی با SiteBranding و عنوان‌ها با PageTitle تنظیم می‌شوند. +- پوسته فارسی و RTL است و از وزیرمتن، Tailwind 4، HTMX و Alpine استفاده می‌کند. Compressor نیز در پروژه موجود است. +- شماره حساب، شناسه واریز و ثبت فیش در مسیرهای فعلی مشتری وجود ندارند؛ خدمات مالی در این طراحی امکانات آینده‌اند. +- این نمونه برند را از فایل لوگوی محلی نمایش می‌دهد و متن‌های پیشنهادی دارد؛ دیتابیس را نمی‌خواند. + +منابع: `config/settings/base.py`، `config/urls.py`، `apps/tickets/customer_urls.py`، `apps/tickets/views.py:customer_list`، `apps/tickets/selectors.py:customer_tickets`، `apps/core/context_processors.py`، `templates/base.html`، `templates/tickets/customer_list.html`، `static/src/tailwind.css`، `package.json` و `docs/THEMING.md`. + +## منطق UI/UX + +خانه یک منوی خدمات قابل‌فهم است. کاربر پیش از دیدن سوابق، خدمت موردنیاز را انتخاب می‌کند. کارت مشترک «ارتباط با کارشناسان» سرمه‌ای با تأکید نارنجی است. عنوان مشترک و دو اقدام کوتاه «ثبت درخواست» و «درخواست‌های من» کاربردهای این خدمت را روشن می‌کنند؛ جملهٔ کامل راهنمای پرسش، درخواست، پیگیری و هماهنگی در موبایل و دسکتاپ نمایش داده می‌شود. ثبت درخواست آبی و مشاهده سوابق با حاشیهٔ روشن نمایش داده می‌شود. خود کارت مقصد کلیک ندارد تا انتخاب کاربر مبهم نشود. سایر کارت‌ها سفیدند؛ خدمات آینده برچسب متنی دارند و توضیحشان خوانا است. + +سوابق فقط پس از انتخاب «درخواست‌های من» دیده می‌شوند. جست‌وجو، فیلتر «همه / در جریان / بسته» و نمایش همه، پیشنهادهای این نمونه‌اند؛ آمار نمایشی نباید به حساب واقعی نسبت داده شود. + +در موبایل کارت مشترک ارتباط با کارشناسان تمام عرض را می‌گیرد و دو اقدام آن کنار هم هستند؛ خدمات مالی در ردیف بعدی دو ستون دارند. در دسکتاپ کارت مشترک دو ستون از شبکهٔ چهارستونه را می‌گیرد و دو اقدام آن کنار هم هستند. + +## وفاداری و جداسازی + +- فونت وزیرمتن، لوگو، فایل CSS کامپایل‌شده Tailwind و رنگ‌های فعلی پروژه استفاده می‌شوند. +- سرمه‌ای `#08376b`، آبی `#0693e3`، نارنجی `#fa9f00`، زمینه `#eeeeee` و کارت‌های سفید حفظ شده‌اند. +- CSS و JavaScript خانه داخل HTML نمونه هستند؛ دو صفحهٔ مالی از `service-pages.css` مستقل در همین پوشه استفاده می‌کنند. +- URL دارایی‌ها نسخه دارد؛ مقدار نسخه هنگام ساخت بر اساس الگوریتم فعلی `static_asset_version`، یعنی بیشترین زمان تغییر فایل‌های static و templates، محاسبه شده است. +- هیچ وابستگی، فونت یا CDN جدید اضافه نشده و build اجرا نشده است. +- Service Worker ثبت نمی‌شود. کش و PWA برنامه اصلی تغییر نکرده‌اند. +- همهٔ فایل‌های نمونه در `design-prototypes/chaharsu/` هستند؛ فایل‌های فعلی templates، static و بک‌اند دست‌نخورده‌اند. + +## تعامل‌های نمونه + +ثبت درخواست لینک به نمونهٔ مستقل موجود فرم در `../ticket-new-wizard/index.html` دارد. درخواست‌های نمونه پنجره توضیح دارند؛ مکالمه واقعی باز نمی‌شود. پروفایل پنجره توضیح دارد. «ارسال فیش واریزی» و «شماره حساب‌ها» با لینک معمولی به صفحات HTML مستقل می‌روند و هر دو صفحه لینک بازگشت به خانه و دسترسی به خدمت مالی دیگر دارند. انتخاب فیش فقط نام فایل را نشان می‌دهد؛ محتوای تصویر برای اعتبارسنجی در مرورگر خوانده می‌شود؛ بارگذاری یا ذخیره نمی‌شود. تمام داده‌ها نمایشی‌اند. + +پنجره‌ها با Escape، دکمه بستن و کلیک بیرون بسته می‌شوند. dialog بومی مدیریت فوکوس را انجام می‌دهد. جابه‌جایی نما فوکوس را به عنوان آن می‌برد. + +## بررسی نسخهٔ اصلاح‌شده + +Playwright موجود در محیط مجازی پروژه برای بررسی مرورگر نمونه استفاده شد: +- خانه فقط خدمات را نشان می‌دهد و سوابق در آن مخفی‌اند. +- کارت مشترک و دو اقدام آن در موبایل ۳۹۰×۸۴۴ بررسی شدند؛ هر سه کارت خدمات بالاتر از نوار پایین قرار دارند. +- نمای جدا درخواست‌ها، جست‌وجو، فیلتر، حالت بدون نتیجه، نمایش همه و بازگشت به خدمات بررسی شدند. +- Back/Forward مرورگر، ناوبری واقعی به دو صفحهٔ مالی، بازگشت به خانه، انتخاب فایل بدون ارسال و حذف انتخاب بررسی شدند. +- در عرض‌های ۳۲۰، ۳۶۰، ۳۹۰، ۶۴۰، ۷۶۸، ۱۰۲۴ و ۱۴۴۰ اسکرول افقی مشاهده نشد. +- خطای JavaScript مشاهده نشد. +- تصاویر خانه و نمای درخواست‌ها به‌روز شدند. + +این بررسی‌ها مخصوص نمونهٔ مستقل‌اند؛ تست یا کد اصلی Django تغییر نکرده است. + +## قرارداد پیاده‌سازی صفحات مالی + +در HTML خانه کنار لینک‌های دو خدمت و در ابتدای هر صفحهٔ مستقل کامنت IMPLEMENTATION NOTE درج شده است: +- برای ارسال فیش و شماره حساب‌ها باید URL نام‌گذاری‌شده، view و template مستقل در Django تعریف شود. +- این لینک‌ها نباید به modal یا data-dialog تبدیل شوند. +- مسیرهای فعلی فایل HTML فقط مسیرهای نمونه‌اند؛ نام مسیرهای نهایی Django هنوز تعیین نشده است. +- پوستهٔ نهایی باید از base.html ارث ببرد و دارایی‌ها با static_asset_version نسخه‌گذاری شوند. +- دکمهٔ ارسال فیش تا پیاده‌سازی بک‌اند، اعتبارسنجی و رسید ثبت غیرفعال است. انتخاب فایل به معنی ثبت پرداخت نیست. +- حساب‌ها و شناسهٔ واریز باید از منبع معتبر بک‌اند بیایند؛ هیچ شمارهٔ واقعی یا ساختگی در نمونه درج نشده است. + +متن کارت فیش: «تصویر فیش پرداخت خود را برای بررسی بفرستید.» +متن کارت شماره حساب‌ها: «شماره حساب، شبا و شناسهٔ واریز را دریافت کنید.» + +## فشرده‌سازی برای گوشی کوچک + +کارت ارتباط با کارشناسان عنوان، توضیح کامل و دو اقدام کنار هم دارد. توضیح تکراری زیر دکمه‌ها حذف شده است. عنوان خانه، فاصله‌ها و متن خدمات مالی نیز کوتاه‌تر شده‌اند؛ هدف دیده شدن همهٔ خدمات پیش از نوار پایین در صفحهٔ نخست گوشی‌های کوچک است. دکمه‌ها حداقل ۴۴ پیکسل ارتفاع دارند. + +بررسی نسخهٔ فشرده: در اندازه‌های ۳۲۰×۵۶۸، ۳۶۰×۶۴۰، ۳۷۵×۶۶۷، ۳۹۰×۸۴۴ و ۴۱۲×۷۳۲ هر سه کارت به‌طور کامل، بدون اسکرول و بالاتر از نوار پایین دیده شدند. عرض‌های ۶۴۰، ۷۶۸، ۱۰۲۴ و ۱۴۴۰ نیز بدون سرریز افقی بررسی شدند. ارتفاع هر دو دکمهٔ درخواست حداقل ۴۴ پیکسل است. مسیر ثبت درخواست، سوابق و صفحات مستقل مالی پس از فشرده‌سازی مجدداً بررسی شدند. تصویر `mobile-small.png` مربوط به گوشی ۳۲۰×۵۶۸ است. + +## عنوان خدمت و امضای توسعه‌دهنده + +عنوان کارت مشترک «ارتباط با کارشناسان» است؛ ثبت درخواست و مشاهدهٔ درخواست‌های من دو مسیر داخل آن هستند. توضیح «سؤال بپرسید، درخواست ثبت کنید یا برای پیگیری و هماهنگی با کارشناسان در ارتباط باشید.» در تمام اندازه‌ها، از جمله موبایل، نمایش داده می‌شود تا سایر خدمات همچنان در نگاه اول دیده شوند. امضای پایین هر سه صفحه: «طراحی و توسعه: محمد جواد فصاحت 09202202129». شماره لینک تماس دارد؛ هیچ تماس خودکاری برقرار نمی‌شود. کامنت HTML یادآوری می‌کند که در نسخهٔ اصلی امضا از تنظیم developer_credit موجود در برند تأمین شود. + +## خوانایی در آخرین نسخه + +عنوان کارت «ارتباط با کارشناسان» است و جملهٔ «سؤال بپرسید، درخواست ثبت کنید یا برای پیگیری و هماهنگی با کارشناسان در ارتباط باشید.» در همهٔ اندازه‌ها دیده می‌شود. توضیحات و دکمه‌ها حداقل ۱۴ پیکسل‌اند، عنوان کارت در موبایل ۱۸ و در دسکتاپ ۲۰ پیکسل است، و برچسب‌های وضعیت، ناوبری و امضا ۱۲ پیکسل‌اند. خط توضیح با ضریب ۱٫۶۵ یا بیشتر تنظیم شده است. روی موبایل متن کارت‌های مالی کوتاه‌تر است و اقدام تکراری انتهای کارت نمایش داده نمی‌شود؛ خود کارت همچنان لینک صفحهٔ مستقل است. در عرض کمتر از ۳۶۰ پیکسل آیکون دکمه‌ها حذف می‌شود تا متن با همان اندازه خوانا جا بگیرد. اندازه‌های ۳۲۰×۵۶۸ تا دسکتاپ بررسی شدند: متن کامل نمایش داده شد، هیچ عنوانی سرریز نداشت و تمام خدمات بالاتر از نوار پایین موبایل قرار داشتند. + +## هماهنگی خوانایی صفحات مالی + +هر دو صفحهٔ شماره حساب‌ها و ارسال فیش به همان الگوی خوانایی خانه ارتقا یافته‌اند: متن‌ها، اطلاعات حساب و اقدام‌ها حداقل ۱۴ پیکسل؛ عنوان بخش‌ها ۱۸ پیکسل در موبایل و ۲۰ پیکسل در دسکتاپ؛ عنوان صفحه ۲۴/۲۸ پیکسل؛ برچسب‌ها و امضا ۱۲ پیکسل. در موبایل فاصله‌ها کمتر و متن‌های تکراری حذف شده‌اند؛ متن مهم پنهان نشده است. توضیح اصلی تمام عرض را می‌گیرد تا به ستون باریک کنار آیکون محدود نشود. اقدام‌های فعال و انتخاب فایل حداقل ۴۴ پیکسل ارتفاع دارند. اندازه و خوانایی متن با فشرده‌سازی فضا قربانی نشده است. + +بررسی صفحات مالی: هر دو صفحه در عرض‌های ۳۲۰، ۳۶۰، ۳۷۵، ۳۹۰، ۶۴۰، ۷۶۸ و ۱۴۴۰ پیکسل، بدون اسکرول افقی یا بریدگی متن بررسی شدند. اندازهٔ متن‌های اصلی حداقل ۱۴ پیکسل و ارتفاع لینک‌های اصلی و انتخاب فایل حداقل ۴۴ پیکسل بود. انتخاب فایل، نام بسیار بلند فایل، حذف انتخاب، غیرفعال‌بودن ارسال و لینک‌های بین دو خدمت و خانه بررسی شدند؛ خطای JavaScript دیده نشد. تصاویر `accounts-small.png` و `receipt-small.png` برای گوشی ۳۲۰×۵۶۸ و تصاویر `accounts-desktop.png` و `receipt-desktop.png` برای دسکتاپ اضافه شدند. صفحات جزئیات مالی به اندازهٔ محتوایشان اسکرول عمودی طبیعی دارند؛ شرط دیده‌شدن همهٔ خدمات در نگاه اول همچنان مربوط به خانهٔ خدمات است. + +## یکدست‌سازی نمای درخواست‌ها + +نمای درخواست‌ها از نوار پایین و کارت کارشناسان به یک مقصد می‌رسد. عنوان ردیف‌ها در موبایل ۱۴ و دسکتاپ ۱۶ پیکسل، شماره درخواست ۱۴، وضعیت و اطلاعات ثانویه ۱۲ پیکسل‌اند. فیلترها، اقدام‌های اصلی و جست‌وجو حداقل ۴۴ پیکسل ارتفاع دارند. جست‌وجو ۱۶ پیکسل است. در موبایل موضوع و اطلاعات در خط بالایی و وضعیت/شماره در ردیف بعد قرار می‌گیرند و امکان wrapping حفظ می‌شود. حالت فعال «درخواست‌ها» در نوار پایین سرمه‌ای روشن‌تر است تا سفید روی آن با قواعد کنتراست راهنما هماهنگ باشد. پنجرهٔ جزئیات نمونهٔ درخواست هم متن اصلی ۱۴ پیکسل دارد. + +## جریان اعلامیهٔ واریز و فیش مرتبط + +نسخهٔ جدید `bank-accounts.html` فهرست عمومی حساب‌های شرکت نیست؛ فقط اعلامیه‌های واریزی را نشان می‌دهد که کارشناس برای همین مشتری فرستاده است. ابتدا موضوع، مبلغ، فاکتور، تاریخ و وضعیت فیش نمایش داده می‌شود. با باز کردن هر کارت، بانک، صاحب حساب، شماره حساب، شبا، کارت، شناسه، مبلغ، فاکتور و توضیحات همان اعلامیه دیده می‌شود. فقط مبلغ، شماره حساب، شبا، شماره کارت و شناسهٔ واریز موجود دکمهٔ کپی تکی دارند؛ مبلغ بدون جداکننده و بدون واحد کپی می‌شود. کپی کامل متن چندخطی شامل همهٔ اطلاعات و واحد ریال است. مقادیر اعلام‌نشده کپی ندارند. بازخورد کپی به موفقیت واقعی وابسته است؛ در صورت منع Clipboard، روش انتخاب دستی متن در پنجرهٔ دسترس‌پذیر ارائه می‌شود. + +`payment-demo-data.js` فقط سه fixture ساختگی با حساب‌های نامعتبر دارد؛ دیتابیس نیست. `payment-services.js` رفتار محلی هر دو صفحه را مدیریت می‌کند؛ هیچ ارسال، ذخیره یا وابستگی شبکه‌ای ندارد. دادهٔ واقعی باید از دیتابیس تأمین و به مشتری مخاطب محدود شود. پنهان بودن جزئیات کارت جای کنترل دسترسی سمت سرور را نمی‌گیرد. + +«ارسال فیش این واریز» فقط شناسهٔ اعلامیه را با پارامتر `notice` به فرم می‌برد. فرم همان مورد را انتخاب می‌کند و مبلغ مورد انتظار، فاکتور، بانک و شناسه را نشان می‌دهد؛ لینک بازگشت، جزئیات همان مورد را باز می‌کند. ورود مستقیم به فرم حالت مرتبط را به‌عنوان گزینهٔ اول و پیشنهادی نشان می‌دهد؛ مستقل همیشه قابل انتخاب است. انتخاب مرتبط بدون مورد، پیام راهنما دارد؛ شناسهٔ نامعتبر با خطای روشن نمایش داده می‌شود و هرگز به اعلامیهٔ دیگری وصل نمی‌شود. تغییر به مستقل، شناسهٔ ارتباط را از فرم و URL حذف می‌کند. مبلغ پرداخت‌شدهٔ مشتری و فایل انتخاب‌شده هنگام تغییر نوع/اعلامیه حفظ می‌شوند. هیچ موفقیت ارسال ساختگی نشان داده نمی‌شود؛ دکمهٔ نمونه برای بررسی کلاینت فعال است؛ ارسال واقعی تا پیاده‌سازی اصلی انجام نمی‌شود. + +شماره‌ها و مبلغ می‌توانند در هر اعلامیه متفاوت باشند. پنل کارشناس، مدل داده، API، مجوزها و ذخیرهٔ فیش در محدودهٔ این طراحی نیستند. یادآوری قراردادهای ضروری در کامنت HTML هر دو صفحه آمده است. + +بررسی نسخهٔ اعلامیه‌ها: هفت عرض ۳۲۰، ۳۶۰، ۳۹۰، ۶۴۰، ۷۶۸، ۱۰۲۴ و ۱۴۴۰، کپی واقعی شبا/مبلغ/متن کامل، باز و بسته شدن کارت‌ها، جست‌وجو، نبود شماره کارت، اتصال دوطرفهٔ اعلامیه و فرم، حذف ارتباط با انتخاب مستقل، حفظ مبلغ/فایل، شناسهٔ نامعتبر، انتخاب دستی هنگام منع Clipboard و Escape بررسی شدند. خطای JavaScript یا درخواست HTTP خارجی مشاهده نشد. `accounts-list-mobile.png` حالت اولیهٔ بسته و تصاویر `accounts-*.png` جزئیات باز را نشان می‌دهند. این بررسی جای تست اتصال واقعی سرور یا تأیید کامل WCAG را نمی‌گیرد. + +## فشرده‌سازی کارت‌ها و اولویت واریز مرتبط + +بانک و صاحب حساب خلاصه‌اند؛ فاکتور و توضیحات در «اطلاعات تکمیلی و توضیحات» همیشه در کارت بازشده دیده می‌شوند. کپی تکی فقط برای پنج نوع مقدار پرداخت موجود است. کپی کامل از تمام داده‌های اعلامیه ساخته می‌شود و بسته بودن بخش تکمیلی بر آن اثر ندارد؛ کامنت اجرای اصلی در HTML و JavaScript ثبت شده است. راهنمای کوتاه درخواست شماره حساب و شناسه از کارشناس در بالای فهرست وجود دارد. در فرم فیش، مرتبط اولین گزینه و پیش‌فرض پیشنهادی است؛ راهنمای غیر اجباری ایجاد اعلامیه همیشه دیده می‌شود. مستقل همچنان قابل انتخاب است. + +## اطلاعات تکمیلی ثابت و تاریخ شمسی فیش + +اطلاعات تکمیلی و توضیحات اکنون بخش ثابت داخل کارت بازشده هستند؛ نیازی به باز کردن بخش دوم نیست و کاربر نمی‌تواند آن بخش را جمع کند. کپی کامل همچنان از تمام داده‌ها تولید می‌شود. + +`receipt-date.js` انتخابگر تاریخ شمسی با سه select استاندارد روز/ماه/سال است؛ کتابخانه یا فایل خارجی ندارد. ماه و سال جاری برای سهولت انتخاب آماده‌اند، ولی روز ابتدا خالی است تا تاریخ واقعی فیش با تأیید کاربر ثبت شود. «امروز» بر اساس منطقهٔ زمانی ایران تاریخ کامل را انتخاب می‌کند. طول ماه‌ها و اسفند با تقویم Persian در Intl مرورگر بررسی می‌شوند؛ تغییر به ماه/سال کوتاه‌تر، روز نامعتبر را پاک و راهنمای انتخاب دوباره را اعلام می‌کند. تاریخ انتخاب‌شده هنگام تغییر نوع فیش یا اعلامیه حفظ می‌شود. در نمونه سال‌های ده سال قبل تا یک سال بعد قابل انتخاب‌اند؛ بازهٔ نسخهٔ اصلی باید با نیاز واقعی تعیین شود. + +مقدار قابل ارسال `paid_date` تاریخ میلادی `YYYY-MM-DD` با ارقام ASCII است؛ انتخابگر همچنان شمسی نمایش داده می‌شود. قرارداد اعتبارسنجی و تبدیل با ابزار جلالی موجود پروژه در کامنت HTML آمده است؛ در نمونه هیچ تاریخ یا فایلی ارسال/ذخیره نمی‌شود. + +بررسی تاریخ: طول ۱۴۴ ماه در تمام سال‌های قابل انتخاب با jdatetime موجود پروژه تطبیق داده شد؛ اسفند کبیسه، حذف روز نامعتبر، دکمهٔ امروز با ساعت ایران، حفظ تاریخ بین حالت‌ها و خوانایی کنترل‌ها در هفت اندازهٔ نمایش بررسی شدند. تصاویر مالی به‌روز شدند. + +## اعتبارسنجی کلاینت فیش + +`receipt-validation.js` فرم را بدون ارسال شبکه‌ای بررسی می‌کند. مبلغ صحیح مثبت ریال (پذیرش ارقام فارسی/عربی و جداکنندهٔ معتبر)، روز/ماه/سال کامل، تاریخ غیرآینده و تصویر فیش الزامی‌اند. مبلغ آمادهٔ ارسال در `paid_amount` رشتهٔ عدد ASCII بدون جداکننده است؛ `paid_date` میلادی و `receipt_image` فایل است. فیش مرتبط به `notice_id` معتبر نیاز دارد؛ در حالت مستقل این select غیرفعال است و شناسه به FormData وارد نمی‌شود. + +حد نمونهٔ تصویر مطابق پیش‌فرض IMAGE در bootstrap پروژه، ۱۰ مگابایت و JPG/JPEG/PNG/GIF/WEBP است. علاوه بر پسوند و MIME، امضای فایل و باز شدن تصویر در مرورگر بررسی می‌شوند؛ نتیجهٔ بررسی فایل در حافظه و فقط برای همان File نگهداری می‌شود. فایل خالی، بزرگ، خراب یا دارای نوع ناسازگار پیام مشخص دارد. محدودیت واقعی در نسخهٔ اصلی باید از AttachmentPolicy سرور دریافت شود، نه این عدد نمونه. + +خطا زیر فیلد، aria-invalid و فهرست خطاهای لینک‌دار نمایش داده می‌شود. با بررسی ناموفق، اولین فیلد دارای خطا فوکوس می‌گیرد و به مرکز نمایش می‌آید؛ برای ورودی فایل پنهان، کنترل فارسی انتخاب تصویر در معرض دید قرار می‌گیرد. پس از اصلاح، خطا پاک می‌شود. ارسال دوباره حین بررسی کنترل می‌شود؛ اگر کاربر هنگام بررسی فایل فرم را تغییر دهد، نتیجهٔ قدیمی معتبر اعلام نمی‌شود. در حالت معتبر، متن صریح «این پیش‌نمایش است؛ فیش ارسال یا ذخیره نشده» نمایش داده می‌شود. اعتبارسنجی مرورگر جای اعتبارسنجی سرور نیست و کامنت قرارداد در HTML آمده است. + +## نمایش مبلغ و انتخاب بانک‌ها + +`receipt-amount.js` هنگام تایپ مبلغ ریالی، جداکنندهٔ هزارگان فارسی اضافه می‌کند. ارقام فارسی، عربی و انگلیسی پذیرفته می‌شوند؛ ورودی غیرعددی/اعشاری به عدد دیگری تبدیل نمی‌شود و اعتبارسنجی خطا می‌دهد. محل انتخاب و نشانگر بر اساس تعداد ارقام حفظ می‌شود؛ Backspace/Delete روی جداکننده گیر نمی‌کنند. `paid_amount` همچنان ریالی و بدون جداکننده است. + +زیر مبلغ، معادل تومان به حروف نمایش داده می‌شود: ۲۸۰٬۰۰۰٬۰۰۰ ریال → «بیست و هشت میلیون تومان». محاسبه با BigInt و تقسیم/باقی‌ماندهٔ دقیق انجام می‌شود؛ مثلاً ۱۲۳۴۵ ریال → «هزار و دویست و سی و چهار و پنج دهم تومان»، بدون گرد کردن. + +`receipt-banks.js` فهرست محلی بانک مبدأ با نام‌های شناخته‌شده، گروه نام‌های قدیمی روی فیش‌ها و «سایر» دارد. مبدأ اختیاری است؛ با سایر، نام بانک اختیاری نیز قابل ورود می‌شود. فهرست بانک‌ها در نسخهٔ اصلی باید به‌صورت متمرکز به‌روز نگه داشته شود. + +حساب مقصد اجباری است و کاربر خودش انتخاب می‌کند؛ انتخاب اعلامیه یا نوع فیش مقصد را تغییر نمی‌دهد. فهرست مقصد فقط نام حساب‌های شرکت را نشان می‌دهد، نه شماره حساب/کارت/شبا. `chaharsuCompanyAccounts` در `payment-demo-data.js` صرفاً fixture با ID و نام است؛ در نسخهٔ اصلی این فهرست باید از دیتابیس حساب‌های فعال/مجاز شرکت بیاید. سرور ID انتخابی را دوباره اعتبارسنجی و نام حساب را از دیتابیس بخواند. این قرارداد در HTML و فایل داده کامنت شده است. + +بررسی مرورگر: گروه‌بندی زنده و اصلاح وسط متن، حذف کنار جداکننده، جایگزینی انتخاب، ارقام عربی، صفرهای ابتدایی، معادل دقیق تومان، خطای مقصد و فوکوس، نام‌های بدون شماره، مبدأ اختیاری/سایر، حفظ انتخاب‌ها و FormData ریالی/میلادی بررسی شدند؛ هفت اندازهٔ نمایش بدون سرریز و هیچ منبع خارجی بودند. diff --git a/design-prototypes/chaharsu/STYLE_GUIDE.md b/design-prototypes/chaharsu/STYLE_GUIDE.md new file mode 100644 index 0000000..5f874ce --- /dev/null +++ b/design-prototypes/chaharsu/STYLE_GUIDE.md @@ -0,0 +1,554 @@ +# راهنمای سبک طراحی چهارسو + +نسخه ۱ · تاریخ ۲۰۲۶/۱۰/۰۵ · برای طراحی صفحات جدید و بازبینی طرح‌های بعدی + +این راهنما زبان بصری نمونهٔ چهارسو را استخراج می‌کند و قواعد استفادهٔ مجدد از آن را مشخص می‌کند. مبنا، **نمای نهایی در مرورگر** است؛ نه اولین قانون مشابه در CSS. خانه، شماره حساب‌ها، ارسال فیش، نمای سوابق و پنجره‌های نمونه بررسی شده‌اند. + +**وضعیت این سند:** راهنمای طراحی است؛ هیچ تغییر اجرایی در صفحات یا کد اصلی پروژه اعمال نمی‌کند. موارد «پیشنهادی» هنوز در همهٔ اجزای نمونه پیاده نشده‌اند. این سند تأییدیهٔ دسترس‌پذیری کل برنامه نیست. + +## ۱. هویت و اصول طراحی + +سبک چهارسو فارسی، راست‌به‌چپ، روشن، ساده و مبتنی بر کارت خدمات است: +- زمینهٔ خاکستری روشن؛ کارت سفید؛ عنوان سرمه‌ای؛ آبی برای تعامل؛ نارنجی برای تأکید محدود. +- صفحهٔ نخست به انتخاب خدمت اختصاص دارد. سوابق و فرم‌های طولانی بعد از انتخاب خدمت نمایش داده می‌شوند. +- هر کارت باید با عنوان، آیکون و توضیح کوتاه قابل فهم باشد؛ رنگ یا آیکون به‌تنهایی حامل معنی نباشند. +- یک مسیر اصلی مشخص و مسیرهای فرعی خوانا داشته باشید. +- برای جمع‌وجور شدن موبایل ابتدا متن تکراری، تزئین و فاصلهٔ اضافی را حذف کنید؛ متن مهم را کوچک یا مخفی نکنید. +- فضای بیشتر دسکتاپ اجازهٔ توضیح و فاصلهٔ بیشتر می‌دهد؛ معنای خدمت در موبایل و دسکتاپ باید یکسان بماند. + +### ساختار مرجع خانه + +| خدمت | ساختار | رفتار | +|---|---|---| +| ارتباط با کارشناسان | کارت سرمه‌ای مشترک با دو اقدام داخلی | ثبت درخواست / درخواست‌های من | +| ارسال فیش واریزی | کارت سفید با آیکون فیش و وضعیت خدمت | رفتن به صفحهٔ مستقل | +| شماره حساب‌ها | کارت سفید با آیکون بانک و وضعیت خدمت | رفتن به صفحهٔ مستقل | + +کارت مشترک کارشناسان خودش مقصد کلیک ندارد؛ دو کنترل داخل آن مقصد را مشخص می‌کنند. کارت‌های مالی لینک هستند. خدمات بعدی می‌توانند به شبکه اضافه شوند؛ نام چهارسو محدودیت چهارتایی ایجاد نمی‌کند. + +## ۲. منابع و شیوهٔ استخراج + +| منبع | کاربرد | +|---|---| +| [index.html](index.html) | خانه، کارت‌ها، ناوبری، SVGها، نمای سوابق، پنجره‌ها | +| [service-pages.css](service-pages.css) | سبک مشترک دو صفحهٔ مالی | +| [bank-accounts.html](bank-accounts.html) | اطلاعات حساب و شناسهٔ واریز | +| [payment-receipt.html](payment-receipt.html) | فرم فیش مستقل و مرتبط | +| [payment-services.js](payment-services.js) | باز شدن اعلامیه، کپی و ارتباط فرم | +| [payment-demo-data.js](payment-demo-data.js) | سه اعلامیهٔ ساختگی، بدون حساب واقعی | +| [منبع تم پروژه](../../static/src/tailwind.css) | توکن‌های رنگ، فونت و رفتار پایهٔ موجود | +| [توضیح تم](../../docs/THEMING.md) | نقش رنگ‌های برند | +| [README نمونه](README.md) | تصمیم‌های UX و مرز نمونه با برنامهٔ اصلی | + +مقادیر اصلی با Playwright و `getComputedStyle` در عرض‌های ۳۲۰ و ۱۴۴۰ پیکسل استخراج شدند. بخش سوابق نیز در حالت فعال بررسی شد. قواعد responsive از سورس CSS تطبیق داده شدند. نسبت‌های کنتراست جدول این سند از رنگ‌های ثابت محاسبه شده‌اند. + +در HTML خانه قوانین قدیمی بنر، یادآوری پروفایل و overrideهای مراحل قبلی باقی مانده‌اند. وجود یک selector به معنی استفاده شدن آن در طرح نهایی نیست. برای طراحی جدید، قواعد نهایی همین سند را مبنا قرار دهید و CSS انباشتهٔ نمونه را کامل کپی نکنید. + +## ۳. رنگ‌ها و نقش معنایی آن‌ها + +### پالت موجود + +| نقش | توکن موجود / نام در نمونه | مقدار | کاربرد | +|---|---|---|---| +| متن و ساختار اصلی | `--color-ink` / `--ink` | `#08376b` | عنوان، کارت کارشناسان، ناوبری | +| سرمه‌ای روشن‌تر | `--color-ink-soft` | `#0b4c8c` | عمق و حالت تعاملی تیره | +| آبی برند | `--color-sea` / `--sea` | `#0693e3` | آیکون، سطح فعال، تأکید تعاملی | +| آبی تیره | `--color-sea-dark` / `--color-blue` | `#1977ce` | لینک، hover، سطح آیکون | +| تأکید نارنجی | `--color-coral` | `#fa9f00` | خط پایین کارت اصلی | +| زمینهٔ صفحه | `--color-cream` | `#eeeeee` | زمینهٔ عمومی | +| زمینهٔ فرعی | `--color-soft` | `#eaeaea` | سطح ثانویه | +| سطح کارت | `--color-paper` | `#ffffff` | کارت و فرم | +| آبی بسیار روشن | `--color-mint` | `#e5f4ff` | سطح آیکون، انتخاب فایل | +| زرد پشتیبان | `--color-sun` | `#ffc451` | پالت موجود؛ در طرح نهایی مصرف محدود | +| متن ثانویه | `--muted` در خانه | `#526c86` | توضیح، برچسب، امضا | +| مرز پایه | `--line` در خانه | `#dce5ed` | کادر کارت‌ها | +| متن روی سرمه‌ای | مقدار محلی | `#cadbea` | توضیح کارت کارشناسان | + +### رنگ‌های اجزای خاص + +| جزء | متن / خط | زمینه | +|---|---|---| +| برچسب «به‌زودی» | متن `#805300`، خط `#f3ddae` | `#fff3da` | +| اطلاع‌رسانی خدمت آینده | متن `#805300`، خط `#f3ddae` | `#fff8eb` | +| ناحیه انتخاب تصویر | خط dashed `#91bfdd` | `#f6fbff` | +| کارت شناسهٔ واریز | خط `#cde6f7` | `#f6fbff` | +| جداکننده اطلاعات | `#e7edf2` | سطح کارت | +| سطح آیکون خدمت آینده | متن `#526c86` | `#f0f3f6` | + +نارنجی را به رنگ عمومی متن و همهٔ دکمه‌ها تبدیل نکنید. خاکستری روشن برای سطح و مرز است، نه متن اصلی. + +### کنتراست: مشاهده و قاعدهٔ طراحی بعدی + +برای متن معمولی نسبت حداقل ۴٫۵:۱ و برای متن بزرگِ واجد شرایط حداقل ۳:۱ معیار WCAG است. ۱۴ پیکسل با وزن بالا همچنان متن بزرگ محسوب نمی‌شود. [مرجع W3C: کنتراست متن](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html) + +| متن / زمینه | نسبت محاسبه‌شده، تقریبی | نتیجه برای متن معمولی | +|---|---:|---| +| سفید / سرمه‌ای `#08376b` | ۱۱٫۸۷:۱ | مناسب | +| `#cadbea` / سرمه‌ای | ۸٫۳۸:۱ | مناسب | +| `#526c86` / سفید | ۵٫۴۶:۱ | مناسب | +| `#526c86` / `#eeeeee` | ۴٫۷۰:۱ | مناسب | +| `#805300` / `#fff3da` | ۶٫۰۵:۱ | مناسب | +| سفید / آبی روشن `#0693e3` | ۳٫۳۴:۱ | برای متن کوچک کافی نیست | +| سفید / آبی تیره `#1977ce` | ۴٫۵۹:۱ | روی همین زمینهٔ ثابت مناسب | +| `#1977ce` / سفید | ۴٫۵۹:۱ | روی همین زمینهٔ ثابت مناسب | +| `#1977ce` / `#eeeeee` | ۳٫۹۶:۱ | برای متن کوچک کافی نیست | +| `#1977ce` / `#e5f4ff` | ۴٫۰۹:۱ | برای متن کوچک کافی نیست | + +این اعداد برای جفت‌رنگ ثابت‌اند؛ شفافیت، hover، disabled و پس‌زمینهٔ واقعی باید جدا بررسی شوند. + +**قاعدهٔ پیشنهادی برای طراحی جدید:** دکمهٔ اصلی با متن سفید روی سرمه‌ای ساخته شود. اگر دکمه آبی لازم است، سفید روی آبی تیره انتخاب و در حالت‌های مختلف بررسی شود. لینک متنی روی زمینهٔ خاکستری یا آبی روشن از سرمه‌ای استفاده کند. آبی روشن همچنان برای سطح آیکون، تزئین و نشان فعال قابل استفاده است. + +دکمهٔ آبی و ناوبری فعال نمونه هنوز از سفید روی آبی روشن استفاده می‌کنند؛ این ظاهر به معنی عبور از معیار کنتراست نیست و نباید بی‌بررسی تکثیر شود. برچسب انتخاب تصویر در نسخهٔ جدید مالی به کنترل فارسی با سفید روی سرمه‌ای تبدیل شده است. + +## ۴. تایپوگرافی + +### خانوادهٔ فونت + +وزیرمتن متغیر، وزن ۱۰۰ تا ۹۰۰، با `font-display: swap`. + +فایل‌های موجود: +- `static/fonts/vazirmatn-arabic-wght-normal.woff2` +- `static/fonts/vazirmatn-latin-wght-normal.woff2` + +نام خانواده در برنامه `Vazirmatn` است؛ نمونه برای جداسازی از `ChaharsuVazirmatn` و `ChaharsuLatin` استفاده می‌کند. این نام‌ها فونت‌های جدید نیستند. کتابخانه یا فونت خارجی لازم نیست. + +### مقیاس مرجع + +| نقش | موبایل | دسکتاپ | وزن | ارتفاع خط | +|---|---:|---:|---:|---:| +| عنوان خانه | ۲۲px | ۲۸px | ۹۰۰ | ۱٫۶ | +| عنوان صفحهٔ خدمت | ۲۴px | ۲۸px | ۹۰۰ | ۱٫۵ | +| عنوان بخش / کارت اصلی | ۱۸px | ۲۰px | ۹۰۰ | ۱٫۶ | +| عنوان کارت مالی | ۱۴px | ۱۷px | ۹۰۰ | حدود ۱٫۷ تا ۱٫۸ | +| توضیح و اطلاعات حساب | ۱۴px | ۱۴px | ۴۰۰ | ۱٫۶۵ تا ۱٫۷ | +| دکمه و لینک اصلی | ۱۴px | ۱۴px | ۷۰۰ تا ۸۰۰ | حدود ۱٫۵ تا ۱٫۷ | +| label انتخاب فایل | ۱۶px | ۱۶px | ۸۰۰ | ۱٫۶ | +| وضعیت، مسیر صفحه، ناوبری موبایل | ۱۲px | ۱۲px | ۴۰۰ تا ۷۰۰ | ۱٫۵ تا ۱٫۸ | +| امضای توسعه‌دهنده | ۱۲px | ۱۲px | ۴۰۰ | ۱٫۸ تا ۱٫۹ | + +اندازه‌ها تصمیم طراحی چهارسو هستند؛ WCAG یک حداقل عمومی «۱۴ پیکسل» برای همهٔ متن‌ها تعیین نمی‌کند. خوانایی به فونت، کنتراست، طول خط، فاصله و امکان بزرگ‌نمایی نیز وابسته است. برای متن طولانی‌تر، ۱۶px با ارتفاع خط حدود ۱٫۷ پیشنهاد می‌شود؛ این حالت در نمونهٔ فعلی صفحهٔ متن طولانی آزمایش نشده است. + +قواعد: +- متن عملیاتی، توضیح اصلی، عنوان دکمه و عدد حساب را از ۱۴px کوچک‌تر نکنید. +- ۱۲px را فقط برای اطلاعات کم‌اولویت به کار ببرید؛ مبلغ، شماره حساب یا پیام خطا اطلاعات کم‌اولویت نیستند. +- حروف فارسی را با letter-spacing فشرده نکنید. +- عنوان‌ها ۸۰۰ یا ۹۰۰؛ متن عادی ۴۰۰؛ دکمه‌ها ۷۰۰ یا ۸۰۰. +- متن چندخطی با خط مناسب نمایش داده شود؛ برای ارتفاع ثابت کارت بریده نشود. +- به‌جای تعداد خط ثابت، توضیح را کوتاه بنویسید و اجازهٔ رشد ارتفاع بدهید. +- واحدهای `rem` برای الگوی جدید ترجیح دارند: ۱۴px برابر ۰٫۸۷۵rem در ریشهٔ ۱۶px. اندازهٔ ریشه را برای جا دادن محتوا کاهش ندهید. + +### نمونهٔ متن + +عنوان: **ارتباط با کارشناسان** + +توضیح، در موبایل و دسکتاپ: +> سؤال بپرسید، درخواست ثبت کنید یا برای پیگیری و هماهنگی با کارشناسان در ارتباط باشید. + +دو اقدام: **ثبت درخواست** / **درخواست‌های من** + +متن موبایل کارت مالی می‌تواند کوتاه‌تر باشد؛ اما جملهٔ اصلی کارت کارشناسان باید کامل و در هر دو اندازه دیده شود. + +## ۵. فاصله‌ها و هندسه + +مقیاس پایهٔ پیشنهادی برای اجزای جدید: **۴، ۸، ۱۲، ۱۶، ۲۰، ۲۴، ۳۲px**. نمونه در چند نقطه برای چیدمان فشرده از ۶، ۱۰، ۱۴ و ۲۲px هم استفاده می‌کند. همهٔ این مقادیر توکن سراسری موجود نیستند. + +| مورد مشاهده‌شده | موبایل | دسکتاپ | +|---|---|---| +| فاصله افقی خانه از لبه | ۱۶px؛ زیر ۳۶۰px، ۱۲px | ۲۴px داخل container | +| فاصله افقی صفحه مالی | ۱۲px | ۲۴px | +| فاصله کارت‌های خانه | ۱۰px | ۱۶px | +| padding کارت کارشناسان | ۱۲px عمودی / ۱۴px افقی | ۲۰px | +| padding کارت مالی خانه | ۱۱px عمودی / ۱۲px افقی | ۲۴px | +| padding کارت جزئیات مالی | ۱۶px؛ زیر ۳۶۰px، ۱۴px | ۲۲px | +| فاصله عنوان تا توضیح | ۶px | ۶ تا ۷px | +| فاصله دو اقدام داخلی | ۸px | ۱۰px | +| فاصله قبل از اقدام‌ها | ۱۰px | ۱۴px | +| ردیف اطلاعات حساب | ۹px عمودی، gap ۸px | ۱۲px عمودی، gap ۱۲px | +| ناحیه انتخاب فایل | ۱۴px عمودی / ۱۲px افقی | ۲۲px عمودی / ۱۸px افقی | + +### گوشه‌ها، خط و سایه + +- کارت خانه: radius ۲۰px موبایل / ۲۲px دسکتاپ. +- کارت جزئیات: ۲۰px موبایل / ۲۳px دسکتاپ. +- دکمهٔ داخلی: حدود ۱۱–۱۲px؛ دکمهٔ پایهٔ فرم موجود ۱۶px. +- ورودی و وضعیت انتخاب فایل: ۱۲px. +- ناحیه upload: ۱۸px و خط dashed یک‌پیکسلی. +- نوار پایین: ۲۲px؛ گزینهٔ فعال داخل آن ۱۶px. +- برچسب وضعیت: گرد یا pill، حدود ۲۰px. +- مرز کارت: ۱px؛ خط تأکید کارت اصلی: ۳px نارنجی. +- کارت‌ها عموماً بدون سایهٔ دائمی‌اند. نوار ثابت یا پنجره به سایه نیاز دارد؛ همهٔ سطوح را برجسته نکنید. +- hover کارت مستقل: حداکثر جابه‌جایی ۳px و سایهٔ ملایم. کارت مشترک با دو اقدام داخلی جابه‌جا نشود. + +برای طراحی جدید مقادیر نزدیک را با خانوادهٔ منظم توکن‌ها مدیریت کنید؛ تفاوت ۲۲/۲۳px از نمونه الزام هویتی نیست. + +## ۶. چیدمان و responsive + +| محدوده | رفتار مرجع | +|---|---| +| کمتر از ۳۶۰px | فاصله افقی ۱۲px؛ حذف آیکون کنار دو دکمهٔ کارشناسان؛ متن ۱۴px حفظ شود | +| تا ۶۳۹px | پوستهٔ موبایل، شبکهٔ دو ستون، ناوبری پایین | +| ۶۴۰ تا ۸۹۹px | شبکهٔ دو ستون؛ ناوبری دسکتاپ | +| از ۹۰۰px | شبکهٔ چهار ستون؛ کارت کارشناسان دو ستون را می‌گیرد | + +حداکثر عرض خانه و header: **۱۱۲۰px**. حداکثر عرض صفحهٔ مالی: **۹۰۰px**. این اندازه‌ها شامل padding و border-box هستند. + +در موبایل، توضیح کارت کارشناسان زیر عنوان تمام عرض را می‌گیرد. متن را در ستون باریک کنار آیکون قرار ندهید. همین الگو برای عنوان و subtitle صفحات مالی استفاده شده است. + +دو اقدام کارشناسان کنار هم باقی می‌مانند. اگر در یک زبان، بزرگ‌نمایی یا عرض خاص متن جا نشد، ابتدا آیکون غیرضروری حذف و سپس دکمه‌ها به یک ستون تبدیل شوند؛ اندازهٔ متن کاهش نیابد. تبدیل در بزرگ‌نمایی قاعدهٔ پیشنهادی است و هنوز در نمونه اجرا/اعتبارسنجی نشده است. + +**هدف خانه:** هر سه خدمت در viewport ۳۲۰×۵۶۸ و اندازه‌های معمول موبایل بالاتر از نوار پایین دیده شوند. این هدف با سه خدمت فعلی آزمایش شده است؛ تضمین برای تعداد نامحدود خدمات یا همهٔ تنظیمات فونت نیست. + +**صفحهٔ جزئیات:** اسکرول عمودی طبیعی مجاز است. فرم، اطلاعات حساب و توضیحات را برای حذف کامل اسکرول کوچک نکنید. + +## ۷. آیکون‌ها و لوگو + +### الزام قطعی: تمام دارایی‌ها داخل پروژه + +هیچ صفحه‌ای نباید برای آیکون، فونت، CSS، JavaScript، تصویر یا لوگو به CDN یا فایل روی دامنهٔ خارجی وابسته باشد. این الزام برای طراحی و پیاده‌سازی آینده نیز پابرجاست. + +آیکون‌های این نمونه با مسیرهای SVG مستقیماً در HTML نوشته شده‌اند و از بستهٔ بیرونی یا سرویس آیکون دریافت نمی‌شوند. تعریف در `…` و نمایش با `` انجام می‌شود؛ ارجاع فقط به شناسهٔ داخل همان سند است. `xmlns="http://www.w3.org/2000/svg"` شناسهٔ استاندارد فضای نام SVG است، نه نشانی دانلود؛ مرورگر برای آن درخواست شبکه نمی‌فرستد. + +اگر در آینده آیکون‌ها در یک فایل مشترک یا include جمع شدند، آن فایل باید در خود مخزن نگهداری و از static همان پروژه سرو شود. URL دارایی‌های محلی برنامه باید با static_asset_version نسخه‌گذاری شود. کتابخانهٔ موردنیاز نیز باید با خروجی محلی داخل پروژه ارائه شود؛ لینک CDN، import از URL، Google Fonts، icon API یا fallback شبکه‌ای مجاز نیست. + +فونت وزیرمتن در static/fonts، لوگو و favicon در static/images و Tailwind کامپایل‌شده در static/css موجودند. JavaScript نمونه داخل HTML است. + +لینک‌های W3C در این سند فقط منابع مطالعه‌اند و وابستگی اجرایی صفحات نیستند. برای بازبینی بعدی، صفحه باید با قطع دسترسی اینترنت و منابع محلی بارگذاری شود؛ در برنامهٔ اصلی اتصال به سرور خود پروژه همچنان ممکن است لازم باشد. + + +آیکون‌های نمونه SVG خطی محلی‌اند؛ بستهٔ آیکون یا فونت آیکون خارجی استفاده نشده است. + +الگوی پایه: +- `viewBox="0 0 24 24"` +- `fill="none"` +- `stroke="currentColor"` +- `stroke-width="1.7"` +- `stroke-linecap="round"` و `stroke-linejoin="round"` +- اندازه متداول ۲۰–۲۴px؛ مقدار پایهٔ نمونه ۲۳px. +- آیکون داخل دکمه ۱۶–۲۰px؛ آیکون نوار پایین ۲۰px. +- قاب آیکون خانه ۲۸–۳۰px موبایل؛ قاب عنوان صفحه ۳۶px موبایل / ۴۸px دسکتاپ. +- اندازهٔ آیکون با اندازهٔ ناحیهٔ قابل لمس یکی نیست. + +| شناسه موجود | مفهوم | +|---|---| +| `i-chat` | ارتباط با کارشناسان | +| `i-plus` | ثبت درخواست | +| `i-history` | درخواست‌های من | +| `i-receipt` | فیش واریزی | +| `i-bank` | شماره حساب‌ها | +| `i-grid` | خدمات | +| `i-user` | پروفایل | +| `i-arrow` | رفتن به مقصد | +| `i-upload` | انتخاب تصویر | +| `i-search` | جست‌وجو | +| `i-home`، `i-shield` | در sprite موجودند؛ در خانهٔ نهایی نقش اصلی ندارند | + +آیکون کنار متن `aria-hidden="true"` باشد. کنترل فقط‌آیکون باید نام قابل فهم مانند `aria-label="بستن پنجره"` داشته باشد. از emoji یا کاراکتر تصادفی به‌عنوان سبک آیکون جدید استفاده نکنید. + +لوگو از `static/images/company-logo.png` یا منبع برند برنامه می‌آید. نسبت طول به عرض حفظ شود؛ `height:auto` و در صورت نیاز `object-fit:contain`. اندازهٔ مشاهده‌شده در desktop ۸۵px است؛ نمونه‌های موبایل خانه و صفحات مالی برای جا دادن header اندازه‌های متفاوت دارند. لوگو را برای ساخت آیکون خدمت تغییر شکل ندهید. + +## ۸. اجزا و رفتار تعاملی + +### کارت خدمت + +عنوان واضح + آیکون مرتبط + توضیح کوتاه + وضعیت، در صورت لزوم. + +- یک مقصد: کل کارت لینک `a` باشد. +- دو اقدام مستقل: ظرف `section` و دو کنترل جدا؛ لینک تو‌در‌تو نسازید. +- «به‌زودی» وضعیت خدمت است؛ برای نمونه می‌تواند به صفحهٔ preview برود. +- قابلیت آماده‌نشده را با شماره حساب یا موفقیت جعلی واقعی جلوه ندهید. +- عنوان کارت به‌جای زبان داخلی سیستم، نتیجهٔ قابل فهم برای مشتری را بیان کند. + +### دکمه‌ها + +- ارتفاع اقدام اصلی حداقل ۴۴px؛ عرض کنترل نیز برای لمس مناسب باشد. +- ۴۴×۴۴px یک هدف طراحی و معیار **Target Size (Enhanced)، سطح AAA** است؛ همهٔ لینک‌های متنی بدون استثنا ملزم به این ابعاد نیستند. [مرجع W3C](https://www.w3.org/WAI/WCAG22/Understanding/target-size-enhanced.html) +- اقدام اصلی پررنگ؛ اقدام ثانویه با border یا زمینهٔ ملایم. +- متن دکمه یک فعل/مقصد مشخص داشته باشد: «ثبت درخواست»، «مشاهده حساب‌ها»، «حذف انتخاب». +- disabled با ویژگی واقعی HTML اعمال شود؛ دلیل غیرفعال بودن نزدیک آن نوشته شود. +- لینک برای ناوبری، button برای اقدام یا بازکردن پنجره. + +### فرم انتخاب فیش + +label ۱۶px؛ ورودی و راهنما ۱۴px؛ ناحیه روشن با کادر dashed. + +- به‌جای چند جمله تکراری، یک راهنمای کیفیت تصویر و یک توضیح دربارهٔ نتیجهٔ انتخاب فایل کافی است. +- نام بلند فایل با `overflow-wrap:anywhere` قابل نمایش باشد. +- انتخاب فایل به معنی ارسال یا تأیید پرداخت نیست. +- در نمونه دکمه فقط اعتبارسنجی می‌کند و فیش ارسال نمی‌شود؛ در برنامهٔ نهایی پس از ثبت واقعی نتیجه نمایش داده شود. +- کنترل file فعلی بومی مرورگر است؛ زبان و ظاهر بخشی از آن وابسته به مرورگر/سیستم عامل است. عبارت انگلیسی آن را جزء زبان بصری فارسی پروژه تلقی نکنید. + +### اطلاعات حساب + +از `dl / dt / dd` برای ارتباط برچسب و مقدار استفاده کنید. متن مقدار حساب ۱۴px یا بیشتر باشد. حساب، شبا و شناسه با جهت LTR و بدون بریدگی نمایش داده شوند. نمونه اکنون اعداد ساختگی نامعتبر دارد؛ نمایش و کپی این مقادیر در اندازه‌های موبایل و دسکتاپ بررسی شده است. دادهٔ حساب واقعی همچنان فقط باید از بک‌اند بیاید. + +کپی جداگانهٔ فیلدها و کپی کامل متن اکنون در نمونه وجود دارد؛ کنترل نام مشخص و بازخورد قابل مشاهده/اعلان وضعیت دارد. در صورت منع کپی خودکار، انتخاب دستی متن ارائه می‌شود. قواعد کامل در بخش «الگوی مالی» آمده‌اند. + +### ناوبری + +خانهٔ خدمات، درخواست‌ها و پروفایل سه مقصد نوار موبایل هستند. از `aria-current="page"` برای مقصد فعال استفاده شود. + +در نمونه: +- نوار پایین با فاصلهٔ افقی ۱۲px و فاصلهٔ پایین حداقل ۱۰px قرار دارد. +- safe-area با `env(safe-area-inset-bottom)` لحاظ می‌شود. +- ارتفاع هر گزینه حداقل ۵۲px است؛ کل نوار در بررسی ۳۲۰px حدود ۶۷px بود. +- خانه برای پایین صفحه ۹۵px padding دارد تا امکان اسکرول محتوا بالاتر از نوار فراهم شود. + +در طراحی جدید، فضای رزروشده باید با ارتفاع واقعی نوار و safe-area هماهنگ باشد. فوکوس، پیام خطا و پایان فرم نباید زیر آن پنهان بمانند. + +### پنجره و وضعیت‌ها + +پنجره فقط برای کار کوتاه و محدود مناسب است. خدمات مالی صفحهٔ مستقل دارند و به modal تبدیل نشوند. + +الگوی موجود: `dialog` بومی، backdrop تیره، عرض حداکثر ۵۲۰px، بستن با Escape/دکمه/کلیک بیرون، اسکرول داخلی در محتوای بلند. برای کنترل بستن جدید، ناحیهٔ لمس ۴۴×۴۴px پیشنهاد می‌شود؛ نمونهٔ فعلی ۴۰×۴۰px است. + +فوکوس نمایان باشد. برای حالت loading، خطا، موفقیت و empty علاوه بر رنگ، متن معنادار نمایش داده شود. سبک loading و خطای کامل فرم مالی هنوز در نمونه تعریف نشده است؛ از ساخت چند نسخهٔ متفاوت بدون توکن مشترک پرهیز کنید. + +## ۹. متن، راست‌به‌چپ و امضا + +- سند `lang="fa" dir="rtl"` داشته باشد. +- اصطلاحات فعلی: «ارتباط با کارشناسان»، «ثبت درخواست»، «درخواست‌های من»، «ارسال فیش واریزی»، «شماره حساب‌ها». +- خانه محل فهرست سوابق نیست؛ آن‌ها در نمای درخواست‌ها قرار دارند. +- جملهٔ راهنمای کارشناسان در تمام اندازه‌ها قابل نمایش بماند. +- متن کارت‌های مالی در موبایل کوتاه‌تر می‌شود؛ وضعیت و مقصد حذف نمی‌شوند. +- از نام متغیر، API، اصطلاح backend یا توضیح پیاده‌سازی در متن محصول استفاده نکنید. +- برای دادهٔ LTR از `bdi dir="ltr"` یا عنصر مستقل با `dir="ltr"` استفاده کنید. +- در CSS جدید `padding-inline`، `margin-inline` و `text-align:start` را ترجیح دهید. +- مثال: `IR…`؛ این مثال شماره حساب واقعی نیست. + +امضای پایین صفحات: +> طراحی و توسعه: محمد جواد فصاحت 09202202129 + +اندازه ۱۲px، وزن ۴۰۰، متن ثانویه و شماره با جهت LTR. لینک تماس `tel:09202202129` فقط با انتخاب کاربر اجرا شود. در نسخهٔ اصلی محتوا از `developer_credit` تنظیمات برند دریافت شود؛ مقدار را در همهٔ templateها تکثیر نکنید. + +## ۱۰. مثال برای ساخت جزء جدید + +این مثال **پیشنهاد برای طراحی بعدی** است؛ کلاس‌هایش در CSS فعلی تعریف نشده‌اند. رنگ دکمه و لینک به‌قصد رفع موارد کنتراست بخش ۳ انتخاب شده است. + +### توکن‌ها و CSS نمونه + +```css +:root { + --ui-ink: var(--color-ink, #08376b); + --ui-page: var(--color-cream, #eeeeee); + --ui-surface: var(--color-paper, #ffffff); + --ui-muted: #526c86; + --ui-border: #dce5ed; + --ui-action: var(--color-ink, #08376b); + --ui-action-hover: var(--color-ink-soft, #0b4c8c); + --ui-link: var(--color-ink, #08376b); + --ui-space-2: .5rem; + --ui-space-3: .75rem; + --ui-space-4: 1rem; + --ui-radius-card: 1.25rem; +} + +.ui-card { + padding: 1rem; + border: 1px solid var(--ui-border); + border-radius: var(--ui-radius-card); + background: var(--ui-surface); + color: var(--ui-ink); +} +.ui-title { + margin: 0; + font-size: 1.125rem; + font-weight: 900; + line-height: 1.6; +} +.ui-description { + margin: .375rem 0 0; + color: var(--ui-muted); + font-size: .875rem; + line-height: 1.7; +} +.ui-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: .5rem; + min-height: 2.75rem; + min-width: 2.75rem; + padding: .5rem .75rem; + border: 1px solid transparent; + border-radius: .75rem; + font: inherit; + font-size: .875rem; + font-weight: 800; + background: var(--ui-action); + color: #fff; +} +.ui-button:hover { background: var(--ui-action-hover); } +.ui-button:focus-visible { + outline: 3px solid var(--color-sea, #0693e3); + outline-offset: 4px; +} +.ui-icon { + width: 1.25rem; + height: 1.25rem; + fill: none; + stroke: currentColor; + stroke-width: 1.7; + stroke-linecap: round; + stroke-linejoin: round; + flex-shrink: 0; +} +@media (min-width: 640px) { + .ui-card { padding: 1.375rem; } + .ui-title { font-size: 1.25rem; } +} +@media (prefers-reduced-motion: reduce) { + .ui-button { transition: none; } +} +``` + +### HTML نمونهٔ صفحهٔ مستقل در Django + +```django +{% extends "base.html" %} +{% load static %} + +{% block content %} +
+

ارسال فیش واریزی

+

+ تصویر فیش را برای بررسی پرداخت ارسال کنید. +

+
+{% endblock %} +``` + +برای CSS جدید از نقطهٔ توسعهٔ مناسب پوسته استفاده شود و URL آن به شکل +`{% static 'css/…' %}?v={{ static_asset_version }}` باشد. `…` مسیر مثال است، نه فایل موجود. این قطعه فرم اجرایی، route یا ذخیرهٔ فایل را پیاده نمی‌کند. + +نمونهٔ آیکون، بعد از تعریف sprite در همان سند: +```html + +``` + +برای ساخت خدمت مستقل، لینک Django باید به URL نام‌گذاری‌شده همان خدمت متصل شود؛ نام routeهای مالی هنوز در پروژه تعریف نشده است. نام فرضی route را به‌عنوان مسیر موجود کپی نکنید. + +## ۱۱. ارتباط با فناوری موجود + +پروژه Tailwind 4، Alpine، HTMX و Compressor را دارد؛ ظاهر این راهنما وابسته به افزودن کتابخانهٔ دیگری نیست. SVGهای محلی و وزیرمتن موجود برای ادامه کافی‌اند. + +در پیاده‌سازی برنامه: +- رنگ‌های برند از توکن‌های مرکزی موجود مصرف شوند. +- کامپوننت مشترک و کلاس‌های معنادار ایجاد شوند؛ CSS تکراری یا overrideهای مرحله‌ای تکثیر نشوند. +- پوستهٔ صفحه از `base.html` و برند از تنظیمات پروژه بیاید. +- فایل‌های محلی با `static_asset_version` نسخه‌گذاری شوند. +- از ثبت Service Worker جدا در صفحهٔ یک خدمت پرهیز شود؛ چرخهٔ PWA متعلق به پوستهٔ برنامه است. +- تغییرات کش/PWA باید با تست‌های مقرر پروژه بررسی شوند. +- نمونهٔ مستقل خارج از templates/static است؛ انتقال آن به برنامهٔ اصلی یک کار اجرایی جدا است. + +## ۱۲. مواردی که نباید از نمونه تکثیر شوند + +| مشاهده | وضعیت واقعی | قاعدهٔ طراحی بعدی | +|---|---|---| +| سفید روی آبی روشن دکمه/ناوبری | کنتراست حدود ۳٫۳۴ | رنگ متن/سطح طبق بخش ۳ اصلاح و دوباره سنجیده شود | +| لینک آبی تیره روی خاکستری روشن | کنتراست حدود ۳٫۹۶ | سرمه‌ای یا رنگ آزموده‌شده مناسب همان زمینه | +| label انتخاب فایل | اصلاح شد: کنترل فارسی سفید روی سرمه‌ای، ارتفاع حداقل ۴۴px | وضعیت focus ورودی پنهان روی label نیز دیده شود | +| نام برند فرعی خانه | ۱۱px؛ صفحات مالی ۱۲px | برای اجزای جدید ۱۲px | +| متن‌های نمای سوابق | اصلاح شد: عنوان ۱۴/۱۶، شماره ۱۴، metadata و وضعیت ۱۲px | همین مقیاس در صفحات بعدی حفظ شود | +| فیلتر سوابق / ورودی جست‌وجو | اصلاح شد: حداقل ارتفاع ۴۴px؛ متن فیلتر ۱۴ و جست‌وجو ۱۶px | برای کنترل جدید حداقل ۴۴px هدف‌گذاری شود | +| بستن پنجره / دکمه پروفایل موبایل | ۴۰px | کنترل جدید ۴۴×۴۴px | +| پنجره‌ها | متن عادی ۱۳ و یادداشت ۱۱px | برای پنجره جدید متن اصلی ۱۴، فرعی ۱۲px | +| قوانین بنر و پروفایل قدیمی | هنوز در CSS، ولی خانه نهایی استفاده نمی‌کند | جزء مرجع نهایی محسوب نشوند | +| اعداد ثابت و اطلاعات نمونه | دادهٔ نمایشی، بدون backend | فقط منبع معتبر حساب/مشتری در نسخهٔ اصلی | +| متن ۱۴px برای همه شرایط | تصمیم نمونه، نه تضمین خوانایی همگانی | بزرگ‌نمایی، متن بلند و دستگاه واقعی نیز بررسی شوند | + +موارد دارای عبارت «اصلاح شد» اکنون در نمونه اجرا شده‌اند؛ سایر موارد همچنان نیازمند بررسی یا اصلاح‌اند. + +## ۱۳. معیار بازبینی طرح بعدی + +- [ ] عنوان و مقصد هر خدمت با نگاه اول مشخص است. +- [ ] متن کامل راهنمای کارشناسان روی موبایل و دسکتاپ دیده می‌شود. +- [ ] متن اصلی و داده‌های مهم حداقل ۱۴px؛ metadata کم‌اولویت حداقل ۱۲px است. +- [ ] کنتراست متن و کنترل روی زمینهٔ واقعی، در حالت عادی و تعاملی سنجیده شده است. +- [ ] رنگ تنها نشانهٔ خطا، انتخاب یا وضعیت نیست. +- [ ] هدف لمس کنترل‌های اصلی ۴۴×۴۴px است. +- [ ] فونت، وزن، stroke آیکون، رنگ و گوشه‌ها با خانوادهٔ این راهنما هماهنگ‌اند. +- [ ] خانه در ۳۲۰×۵۶۸، ۳۶۰×۶۴۰، ۳۹۰×۸۴۴ و دسکتاپ بازبینی شده است. +- [ ] متن بلند، عنوان چندخطی، نام فایل بلند و عدد LTR سرریز ندارند. +- [ ] صفحهٔ جزئیات اسکرول طبیعی دارد و فرم پشت نوار ثابت پنهان نیست. +- [ ] Tab، Enter و Escape، فوکوس نمایان و بازگشت مرورگر بررسی شده‌اند. +- [ ] بزرگ‌نمایی متن تا ۲۰۰٪ بدون از دست‌رفتن محتوا یا عملکرد بررسی شده است. +- [ ] حرکت با `prefers-reduced-motion` محدود می‌شود. +- [ ] خدمت مستقل با لینک و صفحه مستقل پیاده می‌شود؛ modal جای آن را نگرفته است. +- [ ] label ورودی، نام کنترل فقط‌آیکون، اعلان نتیجه و پیام خطا وجود دارند. +- [ ] دادهٔ نمایشی، placeholder و وضعیت به‌زودی از اطلاعات واقعی قابل تشخیص‌اند. +- [ ] برند، امضای توسعه‌دهنده و نسخهٔ دارایی‌ها از منبع مشترک برنامه می‌آیند. + +بزرگ‌نمایی ۲۰۰٪ معیار مستقل W3C است؛ تعیین font-size مناسب جای بررسی آن را نمی‌گیرد. [مرجع W3C: تغییر اندازه متن](https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html) + +**حدود بررسی فعلی:** استخراج computed style، اندازه‌های نمایش، نمونه‌های رنگ و بررسی‌های مرورگر قبلی مبنای سند هستند. تست کامل screen reader، تمامی حالت‌های خطا/loading، بزرگ‌نمایی ۲۰۰٪، حساب واقعی و ارزیابی کامل WCAG در این نوبت انجام نشده‌اند. + +## ۱۴. نمونه‌های بصری + +برای مشاهدهٔ زبان بصری فعلی، با درنظرگرفتن موارد نیازمند اصلاح بخش ۱۲: +- [خانه در گوشی کوچک](mobile-small.png) +- [خانه دسکتاپ](desktop.png) +- [شماره حساب‌ها در گوشی کوچک](accounts-small.png) +- [شماره حساب‌ها دسکتاپ](accounts-desktop.png) +- [ارسال فیش در گوشی کوچک](receipt-small.png) +- [ارسال فیش دسکتاپ](receipt-desktop.png) + +برای هر صفحهٔ جدید، ابتدا همین نقش‌ها و اجزا را انتخاب کنید؛ سپس فقط آنچه نیاز مشخص صفحه است اضافه کنید. + +### به‌روزرسانی نمای درخواست‌ها + +نمای درخواست‌ها با مقیاس تایپوگرافی این سند هماهنگ شد. شمارهٔ درخواست از metadata مهم‌تر است و ۱۴ پیکسل باقی می‌ماند. روی موبایل عنوان و اطلاعات بالاتر از وضعیت و شماره قرار می‌گیرند تا متن مجبور به ستون باریک نشود. رنگ فعال گزینهٔ درخواست‌ها در نوار پایین از آبی روشن به ink-soft تغییر کرده است؛ رنگ فعال سایر گزینه‌ها هنوز باید طبق جدول کنتراست بررسی شود. + +### الگوی مالی: اعلامیهٔ واریز اختصاصی مشتری + +صفحهٔ «شماره حساب‌ها» فقط موارد اعلام‌شده توسط کارشناس به مشتری را نمایش می‌دهد؛ حساب عمومی یا حساب پیش‌فرض جایگزین ندارد. هر اعلامیه یک کارت بازشونده با موضوع، مبلغ و واحد، فاکتور، تاریخ و وضعیت فیش است. شماره‌های بانکی در فهرست بسته نمایش داده نمی‌شوند؛ باز شدن کارت همهٔ جزئیات همان مورد را نشان می‌دهد. هویت حساب و مبلغ به هر اعلامیه تعلق دارند، نه به کل صفحه. + +- مبلغ کارت: موبایل ۱۸px، دسکتاپ ۲۰px، وزن ۹۰۰؛ شماره‌های جزئیات ۱۴/۱۶px، راست‌چین با جهت LTR و شکست خط امن. +- عنوان کارت ۱۴/۱۶px، توضیح و کنترل کپی ۱۴px؛ label فرم و مقدار ورودی ۱۶px؛ وضعیت و تاریخ ۱۲px. +- فاصلهٔ کارت‌ها ۱۲px؛ گوشه ۲۰px؛ ردیف اطلاعات در دسکتاپ label/value/copy و در موبایل label بالای value با دکمهٔ کپی کنار آن است. +- دکمهٔ «کپی» حداقل ۴۴px؛ aria-label نام همان فیلد را مشخص کند. مقدار خالی «اعلام نشده» نمایش داده شود و دکمهٔ کپی نداشته باشد. +- عدد کپی‌شده فاقد جداکنندهٔ نمایشی است؛ مبلغِ کپی جداگانه بدون واحد است. «کپی کامل اطلاعات» متن چندخطی خوانا شامل موضوع، تاریخ، شناسهٔ اعلامیه، مبلغ و واحد، بانک، صاحب حساب، شبا، حساب، کارت، شناسهٔ واریز، فاکتور و توضیحات فراهم کند. در نمونه، هشدار ساختگی بودن هم در متن کپی قرار دارد. +- پیام «کپی شد» فقط پس از کپی موفق نمایش داده شود، به screen reader اعلام شود و تمرکز روی کنترل حفظ شود. اگر کپی خودکار ممکن نیست، textarea فقط‌خواندنی برای انتخاب دستی در dialog با عنوان، کنترل بستن و پشتیبانی Escape نمایش داده شود. +- وضعیت «فیش در حال بررسی» به معنی تأیید پرداخت نیست؛ هر وضعیت نام متنی داشته باشد و به رنگ وابسته نباشد. +- ارسال فیش عمل اصلی و کپی کامل عمل ثانویه است؛ هر دو در پایین جزئیات همان کارت باشند. در موبایل تمام عرض و در دو ردیف قرار گیرند. + +### الگوی فرم فیش مستقل و مرتبط + +دو گزینهٔ radio با label واضح و تمام سطح قابل لمس: «واریز مستقل» و «واریز مرتبط». ورود از اعلامیه، گزینهٔ مرتبط و همان اعلامیه را از ابتدا انتخاب می‌کند؛ ورود مستقیم گزینهٔ مرتبط را به‌عنوان اولین گزینهٔ پیشنهادی انتخاب می‌کند؛ مستقل همواره قابل انتخاب است. انتخاب مرتبط، select و خلاصهٔ مبلغ اعلام‌شده، فاکتور، بانک و شناسه را نشان دهد. لینک مشاهدهٔ جزئیات به همان اعلامیه بازگردد. انتخاب مستقل ارتباط قبلی را حذف کند؛ فایل و مبلغ پرداخت‌شده را بی‌دلیل پاک نکند. + +مبلغ مورد انتظار و مبلغ واقعاً پرداخت‌شده دو مفهوم متفاوت‌اند. مبلغ مشتری را با تغییر اعلامیه خودکار بازنویسی نکنید. واحد «ریال» کنار label مبلغ بماند. ورودی مبلغ و پیگیری در موبایل صفحه‌کلید عددی دارند. کنترل انتخاب تصویر، عنوان فارسی و سطح حداقل ۴۴px دارد؛ نام فایل انتخاب‌شده و عبارت «ارسال نشده» مستقل نمایش داده شوند. اطلاعات طولانی wrap شوند؛ حذف اسکرول عمودی با کوچک کردن متن مجاز نیست. + +رنگ متن و لینک این اجزا روی زمینهٔ خاکستری/آبی روشن سرمه‌ای است؛ دکمهٔ اصلی سفید روی سرمه‌ای، ثانویه سرمه‌ای روی سفید با مرز مشخص. فایل‌ها و SVGها کاملاً محلی هستند. این الگو دیتابیس یا مجوزهای سرور را پیاده‌سازی نمی‌کند؛ قرارداد مربوط به دریافت اختصاصی اعلامیه و ارتباط معتبر فیش در کامنت HTML ثبت شده است. + +### نسخهٔ فشردهٔ اطلاعات مالی + +در کارت بازشده فقط مبلغ، حساب، شبا، کارت و شناسهٔ واریز موجود، دکمهٔ کپی تکی دارند. بانک و صاحب حساب در سطر خلاصه نمایش داده شوند؛ فاکتور و توضیحات در بخش ثابت با عنوان «اطلاعات تکمیلی و توضیحات» نمایش داده شوند؛ این بخش کنترل جمع‌شونده ندارد. شماره‌های اعلام‌نشده از فهرست اصلی حذف شوند. کنترل باز و بسته شدن خود کارت اعلامیه حداقل ۴۴px و با صفحه‌کلید قابل استفاده باشد. اطلاعات تکمیلی ۱۴px، راهنمای صفحه ۱۴px با ارتفاع خط ۱٫۷؛ تعداد دکمه‌ها با حذف کپی‌های کم‌کاربرد کاهش یابد، نه با کوچک کردن سطح لمس. + +«کپی کامل اطلاعات» همواره از مدل کامل اعلامیه تولید شود، مستقل از نمایش یا پنهان بودن هر بخش. بانک، صاحب حساب، فاکتور، توضیحات و تمام مقادیر اعلام‌شده باید در متن کامل باقی بمانند. این قرارداد در HTML و JavaScript نیز کامنت دارد. + +در فرم فیش، «واریز مرتبط» همیشه در ترتیب DOM و نمایش اولین گزینه، با نشان متنی «پیشنهادی» و تأکید بصری محدود است. پیش‌فرض ورود مستقیم مرتبط است؛ مستقل برای مشتری بدون اعلامیه نیز در دسترس بماند. راهنمای کوتاه و همیشه قابل مشاهده توضیح دهد: ایجاد اعلامیه توسط کارشناس برای ثبت دقیق‌تر توصیه می‌شود و اجباری نیست. + +### تاریخ شمسی و اطلاعات تکمیلی همیشه نمایان + +اطلاعات تکمیلی هر اعلامیه، پس از باز کردن کارت اصلی، بدون کنترل بازشوندهٔ دیگری نمایش داده شود. عنوان بخش ۱۴px/وزن ۸۰۰، متن ۱۴px با ارتفاع خط ۱٫۸؛ همچنان کپی تکی مخصوص مقادیر پرداخت و کپی کامل شامل تمام داده‌هاست. + +انتخابگر تاریخ فیش: سه select استاندارد «روز، ماه، سال»، RTL، labelهای ۱۴px، مقادیر ۱۶px و ارتفاع حداقل ۴۴px. فاصلهٔ ستون‌ها ۸px موبایل / ۱۲px دسکتاپ؛ نام ماه‌ها و ارقام نمایش فارسی باشند. این انتخاب برای ورود تاریخ معلوم روی فیش ساده است و تعامل‌های پیچیدهٔ wheel سفارشی یا تقویم بزرگ را به فرم اضافه نمی‌کند. ظاهر فهرست بازشدهٔ select تابع مرورگر/سیستم‌عامل است؛ در برخی گوشی‌ها کنترل بومی ممکن است به‌صورت چرخشی نمایش داده شود. هیچ wheel سفارشی، CDN یا کتابخانهٔ تاریخ لازم نیست. + +روز ابتدا خالی؛ ماه/سال جاری برای سهولت آماده‌اند. دکمهٔ «امروز» با منطقهٔ زمانی `Asia/Tehran` و یک بازخورد متنی کامل مثل «۱۳ مهر ۱۴۰۵» انتخاب را روشن می‌کند. طول ماه‌ها از تقویم شمسی مرورگر بررسی شود، نه فرض کبیسهٔ هر چهار سال. روز نامعتبر با تغییر ماه/سال پاک شود و پیام ۱۴px با aria-live کاربر را به انتخاب دوباره راهنمایی کند؛ روز واقعی پرداخت نباید خودکار به روز دیگری تبدیل شود. + +دادهٔ خروجی نمونه تاریخ میلادیِ فقط‌روز در `paid_date` است؛ سرور باید آن را اعتبارسنجی کند. تاریخ پرداخت مستقل از تاریخ اعلامیه است و با تغییر اعلامیه یا نوع فیش نباید بازنویسی شود. + +### خطا و اعتبارسنجی فرم فیش + +مبلغ، تاریخ کامل و تصویر با برچسب اجباری مشخص شوند. خطای هر فیلد با متن فارسی سادهٔ ۱۴px و ارتفاع خط ۱٫۸، رنگ `#9f1f1f` و aria-describedby به کنترل متصل شود. کنترل نامعتبر aria-invalid و مرز قرمز داشته باشد؛ رنگ به‌تنهایی کافی نیست. خطا با اصلاح مقدار به‌روز/پاک شود؛ خطای همهٔ فیلدها قبل از تعامل نشان داده نشود. + +پس از تلاش ناموفق، فهرست لینک‌دار خطاها در بالای فرم و فوکوس روی اولین کنترل نامعتبر ارائه شود. لینک خطا همان کنترل را فوکوس و به نمایش بیاورد؛ برای فایل پنهان، label فارسی قابل لمس نیز در معرض دید قرار گیرد. اسکرول با prefers-reduced-motion هماهنگ باشد. همهٔ مقادیر واردشده حفظ شوند. + +نمونه دکمهٔ «بررسی و ادامه» دارد؛ پس از بررسی معتبر، به‌روشنی اعلام می‌کند هیچ فایلی ارسال نشده است. نتیجهٔ معتبر نباید با موفقیت ثبت پرداخت اشتباه شود. محدودیت فایل در UI و اعتبارسنجی از یک منبع معتبر سرور در نسخهٔ اصلی تغذیه شوند. + +### مبلغ با هزارگان و بانک‌های پرداخت + +مبلغ همیشه با label ریال نمایش داده شود. جداکنندهٔ هزارگان فقط نمایش است؛ مقدار آمادهٔ ارسال ریالی و بدون جداکننده باقی می‌ماند. زیر ورودی، معادل به حروف تومان با متن ۱۴px، وزن ۷۰۰، رنگ سرمه‌ای و ارتفاع خط ۱٫۸ نمایش داده شود. تبدیل از عدد صحیح دقیق و باقی‌مانده استفاده کند؛ بخش کمتر از یک تومان را به «دهم تومان» بیان کند و گرد نکند. هنگام ویرایش، نشانگر و انتخاب متن باید نسبت به ارقام حفظ شوند و حذف روی جداکننده گیر نکند. + +بانک مبدأ با select اختیاری و گزینهٔ «سایر»؛ حساب مقصد با select اجباری و label «به کدام حساب ما واریز کردید؟». گزینه‌های مقصد فقط نام نمایشی حساب را نشان دهند؛ شماره‌های بانکی در فهرست نباشند. حساب از دیتابیس شرکت تأمین شود و ID آن ارسال شود؛ انتخاب مشتری با تغییر اعلامیه/نوع فیش بازنویسی نشود. فیلدها در دسکتاپ دو ستون و در موبایل یک ستون، متن کنترل ۱۶px و ارتفاع حداقل ۴۴px باشند. diff --git a/design-prototypes/chaharsu/accounts-desktop.png b/design-prototypes/chaharsu/accounts-desktop.png new file mode 100644 index 0000000..41a96a3 Binary files /dev/null and b/design-prototypes/chaharsu/accounts-desktop.png differ diff --git a/design-prototypes/chaharsu/accounts-list-mobile.png b/design-prototypes/chaharsu/accounts-list-mobile.png new file mode 100644 index 0000000..caecf38 Binary files /dev/null and b/design-prototypes/chaharsu/accounts-list-mobile.png differ diff --git a/design-prototypes/chaharsu/accounts-mobile.png b/design-prototypes/chaharsu/accounts-mobile.png new file mode 100644 index 0000000..756cadf Binary files /dev/null and b/design-prototypes/chaharsu/accounts-mobile.png differ diff --git a/design-prototypes/chaharsu/accounts-small.png b/design-prototypes/chaharsu/accounts-small.png new file mode 100644 index 0000000..a9165c1 Binary files /dev/null and b/design-prototypes/chaharsu/accounts-small.png differ diff --git a/design-prototypes/chaharsu/bank-accounts.html b/design-prototypes/chaharsu/bank-accounts.html new file mode 100644 index 0000000..6fc2cb8 --- /dev/null +++ b/design-prototypes/chaharsu/bank-accounts.html @@ -0,0 +1,42 @@ + + +شماره حساب‌ها · چهارسو + + + + +
پیش‌نمایش طراحی چهارسو · داده‌ها ساختگی‌اند؛ پرداخت و ارسال واقعی انجام نمی‌شود.
+ +
+ +

شماره حساب‌ها

اطلاعات واریزهای اعلام‌شده توسط کارشناس.

+

از کارشناس بخواهید شماره حساب‌ها و شناسهٔ واریز مرتبط را برای شما ارسال کند.

+

واریزهای شما

+ +
+ + +
+ + + +
+
+

کپی اطلاعات

کپی خودکار در این مرورگر ممکن نشد. متن را انتخاب کنید و از گزینهٔ کپی دستگاه استفاده کنید.

+ diff --git a/design-prototypes/chaharsu/desktop.png b/design-prototypes/chaharsu/desktop.png new file mode 100644 index 0000000..86b24e8 Binary files /dev/null and b/design-prototypes/chaharsu/desktop.png differ diff --git a/design-prototypes/chaharsu/index.html b/design-prototypes/chaharsu/index.html new file mode 100644 index 0000000..e788be5 --- /dev/null +++ b/design-prototypes/chaharsu/index.html @@ -0,0 +1,491 @@ + + + + + + +چهارسو · پیش‌نمایش مرکز خدمات مشتریان + + + + + + +
پیش‌نمایش طراحی چهارسو · اطلاعات نمونه است و هیچ درخواستی ارسال نمی‌شود.
+
+ +
+
+
+
+

خدمات چهارسو

+ +
+ +
+ + +

طراحی و توسعه: محمد جواد فصاحت 09202202129

+
+ + +
+

+ +
+ + + diff --git a/design-prototypes/chaharsu/mobile-small.png b/design-prototypes/chaharsu/mobile-small.png new file mode 100644 index 0000000..7b6b705 Binary files /dev/null and b/design-prototypes/chaharsu/mobile-small.png differ diff --git a/design-prototypes/chaharsu/mobile.png b/design-prototypes/chaharsu/mobile.png new file mode 100644 index 0000000..5e54265 Binary files /dev/null and b/design-prototypes/chaharsu/mobile.png differ diff --git a/design-prototypes/chaharsu/payment-demo-data.js b/design-prototypes/chaharsu/payment-demo-data.js new file mode 100644 index 0000000..769bb8b --- /dev/null +++ b/design-prototypes/chaharsu/payment-demo-data.js @@ -0,0 +1,17 @@ +/* DESIGN FIXTURES ONLY. These are invalid bank numbers, never payment destinations. + Production: supply only payment notices addressed to the authenticated customer. + Account, card, IBAN, deposit reference, expected amount and invoice belong to EACH notice. + Do not replace this with a client-side list of all company bank accounts. */ +window.chaharsuPaymentNotices = [ + {id:'DEMO-101', title:'پرداخت فاکتور خرید لوله', invoice:'DEMO-F-1405-218', issued:'۱۴۰۵/۰۷/۱۲', author:'واحد فروش', amount:'280000000', currency:'ریال', bank:'بانک ملت (نمونه)', holder:'لوله گستر گلپایگان (نمونه)', account:'000000000101', iban:'IR000000000000000000000101', card:'0000000000000101', deposit:'000101218', description:'برای فاکتور اعلام‌شده واریز کنید و شناسهٔ واریز همین مورد را درج کنید. پس از پرداخت، تصویر فیش را از دکمهٔ زیر بفرستید.', receiptReceived:false}, + {id:'DEMO-102', title:'پرداخت هزینهٔ حمل سفارش', invoice:'DEMO-F-1405-219', issued:'۱۴۰۵/۰۷/۱۱', author:'واحد مالی', amount:'18500000', currency:'ریال', bank:'بانک تجارت (نمونه)', holder:'لوله گستر گلپایگان (نمونه)', account:'000000000202', iban:'IR000000000000000000000202', card:null, deposit:'000202219', description:'این مورد مربوط به هزینهٔ حمل است و شماره حساب و شناسهٔ آن با فاکتور خرید متفاوت است.', receiptReceived:false}, + {id:'DEMO-103', title:'تسویهٔ ماندهٔ فاکتور', invoice:'DEMO-F-1405-203', issued:'۱۴۰۵/۰۷/۰۹', author:'واحد مالی', amount:'96000000', currency:'ریال', bank:'بانک ملی (نمونه)', holder:'لوله گستر گلپایگان (نمونه)', account:'000000000303', iban:'IR000000000000000000000303', card:'0000000000000303', deposit:'000303203', description:'فیش قبلی این مورد دریافت شده و در انتظار بررسی کارشناس است. دریافت فیش به معنی تأیید نهایی پرداخت نیست.', receiptReceived:true} +]; + +/* DESTINATION FIXTURES ONLY: production must query authorized, active company accounts from the database. + Return opaque ID + display_name only to this dropdown; no account/card/IBAN numbers. */ +window.chaharsuCompanyAccounts=[ + {id:'DEMO-ACC-101',name:'حساب فروش شرکت — ملت'}, + {id:'DEMO-ACC-202',name:'حساب هزینهٔ حمل — تجارت'}, + {id:'DEMO-ACC-303',name:'حساب تسویهٔ شرکت — ملی'} +]; diff --git a/design-prototypes/chaharsu/payment-receipt.html b/design-prototypes/chaharsu/payment-receipt.html new file mode 100644 index 0000000..9418c6b --- /dev/null +++ b/design-prototypes/chaharsu/payment-receipt.html @@ -0,0 +1,80 @@ + + +ارسال فیش واریزی · چهارسو + + + + +
پیش‌نمایش طراحی چهارسو · داده‌ها ساختگی‌اند؛ پرداخت و ارسال واقعی انجام نمی‌شود.
+ +
+ +

ارسال فیش واریزی

فیش را مستقل یا برای یکی از واریزهای اعلام‌شده ارسال کنید.

+ +
+ +

این فیش مربوط به کدام واریز است؟

+
نوع فیش واریزی + +
+

برای ثبت دقیق‌تر، از کارشناس بخواهید اعلامیهٔ واریز ایجاد کند؛ این کار توصیه می‌شود و اجباری نیست.

+ + +

اگر اطلاعات واریز را بیرون از چهارسو دریافت کرده‌اید، این گزینه را انتخاب کنید.

+

اطلاعات فیش پرداخت

مبلغ، حساب مقصد، تاریخ کامل و تصویر فیش الزامی‌اند.

+

مبلغی که واقعاً پرداخت کرده‌اید؛ ممکن است با مبلغ اعلام‌شده متفاوت باشد.

+ + +
+

نام حسابی را که وجه به آن واریز کرده‌اید انتخاب کنید.

+ +
تاریخ واریز (شمسی · اجباری) +
+

تاریخ درج‌شده روی فیش را انتخاب کنید.

+

روز پرداخت را انتخاب کنید.

+ + + +
+
تصویر JPG، PNG، GIF یا WEBP؛ حداکثر ۱۰ مگابایت.
+ + + + +
نوع فیش: واریز مستقل
+

+ +
+ + +
+
+

کپی اطلاعات

کپی خودکار در این مرورگر ممکن نشد. متن را انتخاب کنید و از گزینهٔ کپی دستگاه استفاده کنید.

+ diff --git a/design-prototypes/chaharsu/payment-services.js b/design-prototypes/chaharsu/payment-services.js new file mode 100644 index 0000000..e069517 --- /dev/null +++ b/design-prototypes/chaharsu/payment-services.js @@ -0,0 +1,159 @@ +/* Local UI prototype. No requests, storage, upload or database writes. + Keep opaque notice IDs in links. Production must check ownership server-side. */ +(() => { +'use strict'; +const version=document.documentElement.dataset.staticVersion; +const notices=window.chaharsuPaymentNotices || []; +const byId=id=>notices.find(notice=>notice.id===id); +const query=selector=>document.querySelector(selector); +const money=notice=>new Intl.NumberFormat('fa-IR').format(Number(notice.amount))+' '+notice.currency; +const fields=notice=>[ + ['مبلغ واریز',money(notice),notice.amount,true], + ['نام بانک',notice.bank,notice.bank,false], + ['صاحب حساب',notice.holder,notice.holder,false], + ['شماره حساب',notice.account,notice.account,true], + ['شماره شبا',notice.iban,notice.iban,true], + ['شماره کارت',notice.card || 'اعلام نشده',notice.card,true], + ['شناسهٔ واریز',notice.deposit || 'اعلام نشده',notice.deposit,true], + ['شماره فاکتور',notice.invoice || 'اعلام نشده',notice.invoice,true], + ['توضیحات کارشناس',notice.description || 'توضیحی ثبت نشده',notice.description,false] +]; +const link=(page,id)=>page+'?v='+version+(id?'¬ice='+encodeURIComponent(id):''); +function copyTextFor(notice){ + return ['اطلاعات نمایشی چهارسو — شماره‌ها ساختگی و غیرقابل پرداخت‌اند.',notice.title, + 'شمارهٔ اعلامیه: '+notice.id,'تاریخ اعلام: '+notice.issued,'اعلام‌کننده: '+notice.author, + ...fields(notice).map(([label,value])=>label+': '+value)].join('\n'); +} +let feedbackTimer; +function feedback(message){ + clearTimeout(feedbackTimer);query('#copy-feedback').textContent=message; + feedbackTimer=setTimeout(()=>{query('#copy-feedback').textContent='';},6500); +} +function fallbackCopy(text){ + const area=document.createElement('textarea');area.value=text;area.setAttribute('aria-label','متن کپی'); + area.style.cssText='position:fixed;top:0;left:0;width:1px;height:1px;opacity:0'; + document.body.append(area);area.select();let copied=false; + try{copied=document.execCommand('copy');}catch(error){copied=false;}finally{area.remove();} + return copied; +} +async function copyValue(text,label){ + const previousFocus=document.activeElement; + let copied=false; + try{if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(text);copied=true;}}catch(error){copied=false;} + if(!copied)copied=fallbackCopy(text); + previousFocus?.focus({preventScroll:true}); + if(copied){feedback(label+' کپی شد. اکنون می‌توانید آن را جای‌گذاری کنید.');return;} + feedback('کپی خودکار ممکن نشد؛ متن آمادهٔ انتخاب است.'); + const dialog=query('#manual-copy');query('#manual-copy-text').value=text; + dialog.showModal();query('#manual-copy-text').focus();query('#manual-copy-text').select(); +} +function appendField(container,field,copyable=true){ + const [label,value,raw,ltr]=field; + const row=document.createElement('div');row.className='payment-field'; + const term=document.createElement('dt');term.textContent=label; + const definition=document.createElement('dd');definition.textContent=value; + if(ltr){definition.dir='ltr';definition.className='numeric-value';} + row.append(term,definition); + if(copyable && raw){ + const button=document.createElement('button');button.type='button';button.className='copy-field'; + button.textContent='کپی';button.setAttribute('aria-label','کپی '+label); + button.addEventListener('click',()=>copyValue(raw,label));row.append(button); + }else{row.classList.add('no-copy');} + container.append(row); +} +function makeNotice(notice){ + const article=query('#notice-template').content.firstElementChild.cloneNode(true); + const button=article.querySelector('.notice-toggle');const detail=article.querySelector('.notice-detail'); + article.id=notice.id;button.id='toggle-'+notice.id;detail.id='details-'+notice.id; + button.setAttribute('aria-controls',detail.id);detail.setAttribute('aria-labelledby',button.id); + article.querySelector('.notice-title').textContent=notice.title; + article.querySelector('.notice-amount').textContent=money(notice); + article.querySelector('.notice-meta').textContent='فاکتور '+notice.invoice+' · '+notice.issued; + const state=article.querySelector('.payment-state'); + state.textContent=notice.receiptReceived?'فیش در حال بررسی':'در انتظار فیش'; + state.classList.toggle('received',notice.receiptReceived); + article.querySelector('.notice-author').textContent='اعلام‌شده توسط '+notice.author; + article.querySelector('.notice-date').textContent='اعلامیهٔ '+notice.id; + article.querySelector('.notice-bank-name').textContent=notice.bank; + article.querySelector('.notice-holder').textContent=notice.holder; + const container=article.querySelector('.essential-fields'); + // Compact UI is a projection; copyTextFor always uses the full notice, including all supplementary information. + const allFields=fields(notice); + [0,3,4,5,6].forEach(index=>{const field=allFields[index];if(field[2])appendField(container,field);}); + const extra=article.querySelector('.extra-fields'); + [7,8].forEach(index=>appendField(extra,allFields[index],false)); + article.querySelector('.copy-complete').addEventListener('click',()=>copyValue(copyTextFor(notice),'اطلاعات کامل')); + article.querySelector('.receipt-link').href=link('payment-receipt.html',notice.id); + button.addEventListener('click',()=>toggleNotice(article,notice)); + return article; +} +function openNotice(article){ + document.querySelectorAll('.payment-notice').forEach(item=>{ + const open=item===article;item.querySelector('.notice-toggle').setAttribute('aria-expanded',String(open)); + item.querySelector('.notice-detail').hidden=!open; + }); +} +function toggleNotice(article,notice){ + const open=article.querySelector('.notice-toggle').getAttribute('aria-expanded')==='true'; + openNotice(open?null:article); + const url=new URL(location.href);url.searchParams.delete('notice'); + if(!open)url.searchParams.set('notice',notice.id); + history.replaceState(null,'',url.href); +} +function initAccounts(){ + const container=query('#payment-notices');notices.forEach(notice=>container.append(makeNotice(notice))); + const search=query('#notice-search'); + function filter(){ + const term=search.value.trim().toLocaleLowerCase('fa');let count=0; + notices.forEach(notice=>{ + const article=document.getElementById(notice.id); + const match=[notice.title,notice.invoice,notice.bank,notice.id].join(' ').toLocaleLowerCase('fa').includes(term); + article.hidden=!match;if(match)count++; + }); + query('#notice-count').textContent=new Intl.NumberFormat('fa-IR').format(count)+' مورد'; + query('#notices-empty').hidden=count!==0; + } + search.addEventListener('input',filter); + query('#clear-search').addEventListener('click',()=>{search.value='';filter();search.focus();});filter(); + if(!notices.length){query('#notices-empty h3').textContent='هنوز اطلاعات واریزی برای شما ارسال نشده است';query('#notices-empty p').textContent='پس از اعلام کارشناس، اطلاعات هر واریز در این بخش نمایش داده می‌شود.';query('#clear-search').hidden=true;search.disabled=true;} + const id=new URLSearchParams(location.search).get('notice'); + if(id){const notice=byId(id);if(notice){openNotice(document.getElementById(id));}else{feedback('این اعلامیه در دسترس نیست. فقط موارد اعلام‌شده به شما نمایش داده می‌شوند.');}} +} +function initReceipt(){ + const select=query('#notice-select'); + notices.forEach(notice=>{const option=document.createElement('option');option.value=notice.id;option.textContent=notice.invoice+' · '+money(notice)+' · '+notice.bank;select.append(option);}); + if(!notices.length)query('#notice-selection-help').textContent='هنوز اعلامیه‌ای ندارید؛ از کارشناس بخواهید ایجاد کند یا واریز مستقل را انتخاب کنید.'; + const modes=document.querySelectorAll('input[name="receipt-mode"]'); + let invalid=false; + function sync(){ + const linked=query('input[name="receipt-mode"]:checked').value==='linked'; + query('#linked-payment').hidden=!linked;query('#independent-help').hidden=linked; + select.disabled=!linked;select.required=linked; + if(!linked){select.value='';invalid=false;} + const notice=linked?byId(select.value):null; + query('#notice-invalid').hidden=!invalid; + select.setAttribute('aria-invalid',String(invalid)); + query('#selected-payment').hidden=!notice; + query('#selected-fields').replaceChildren(); + if(notice){ + query('#selected-title').textContent=notice.title; + [['مبلغ اعلام‌شده',money(notice),false],['شماره فاکتور',notice.invoice,true],['بانک مقصد',notice.bank,false],['شناسهٔ واریز',notice.deposit || 'اعلام نشده',true]].forEach(([label,value,ltr])=>appendField(query('#selected-fields'),[label,value,null,ltr],false)); + query('#selected-details').href=link('bank-accounts.html',notice.id); + } + query('#receipt-summary').textContent=linked?(notice?'این فیش به فاکتور '+notice.invoice+' مرتبط می‌شود.':'برای فیش مرتبط، ابتدا واریز اعلام‌شده را انتخاب کنید.'):'نوع فیش: واریز مستقل؛ ارتباطی با اعلامیه‌های قبلی ندارد.'; + const url=new URL(location.href);url.searchParams.delete('notice');if(notice)url.searchParams.set('notice',notice.id); + history.replaceState(null,'',url.href); + } + const incoming=new URLSearchParams(location.search).get('notice'); + if(incoming){query('input[value="linked"]').checked=true;if(byId(incoming)){select.value=incoming;}else{invalid=true;}} + modes.forEach(mode=>mode.addEventListener('change',sync));select.addEventListener('change',()=>{invalid=false;sync();});sync(); + const input=query('#receipt-file');const selection=query('#file-selection');const status=query('#file-status'); + input.addEventListener('change',()=>{const file=input.files[0];selection.hidden=!file;status.textContent=file?'فایل انتخاب‌شده: '+file.name+' · ارسال نشده':'';}); + query('#clear-file').addEventListener('click',()=>{input.value='';selection.hidden=true;status.textContent='';input.focus();}); + query('#receipt-form').addEventListener('submit',event=>event.preventDefault()); +} +query('#select-copy-text').addEventListener('click',()=>{query('#manual-copy-text').focus();query('#manual-copy-text').select();}); +query('#close-copy').addEventListener('click',()=>query('#manual-copy').close()); +if(document.body.dataset.financePage==='accounts')initAccounts(); +if(document.body.dataset.financePage==='receipt')initReceipt(); +})(); diff --git a/design-prototypes/chaharsu/receipt-amount.js b/design-prototypes/chaharsu/receipt-amount.js new file mode 100644 index 0000000..8cefc67 --- /dev/null +++ b/design-prototypes/chaharsu/receipt-amount.js @@ -0,0 +1,77 @@ +/* Local amount presentation: never convert rials to tomans for the server. + Exact BigInt arithmetic; preserve fractional toman (one rial = one tenth toman). + Visible grouping and Persian words do not replace paid_amount validation. */ +(() => { +'use strict'; +const input=document.querySelector('#paid-amount');if(!input)return; +const words=document.querySelector('#amount-words'); +const small=['صفر','یک','دو','سه','چهار','پنج','شش','هفت','هشت','نه','ده','یازده','دوازده','سیزده','چهارده','پانزده','شانزده','هفده','هجده','نوزده']; +const tens=['','','بیست','سی','چهل','پنجاه','شصت','هفتاد','هشتاد','نود']; +const hundreds=['','صد','دویست','سیصد','چهارصد','پانصد','ششصد','هفتصد','هشتصد','نهصد']; +const scales=['','هزار','میلیون','میلیارد','تریلیون','کوادریلیون']; +const ascii=value=>value.replace(/[۰-۹]/g,digit=>String(digit.charCodeAt(0)-1776)).replace(/[٠-٩]/g,digit=>String(digit.charCodeAt(0)-1632)); +const digitCount=value=>(ascii(value).match(/\d/g) || []).length; +const separators=/[,٬ \u00a0]/g; +const grouped=value=>value.replace(/\B(?=(\d{3})+(?!\d))/g,'٬').replace(/\d/g,digit=>'۰۱۲۳۴۵۶۷۸۹'[digit]); +function threeDigits(value){ + const parts=[]; + if(value>=100)parts.push(hundreds[Math.floor(value/100)]); + const rest=value%100; + if(rest && rest<20)parts.push(small[rest]); + if(rest>=20){parts.push(tens[Math.floor(rest/10)]);if(rest%10)parts.push(small[rest%10]);} + return parts.join(' و '); +} +function numberWords(value){ + if(value===0n)return 'صفر'; + const parts=[];let index=0; + while(value>0n){ + const group=Number(value%1000n); + if(group){const text=group===1 && index===1?'هزار':threeDigits(group)+(scales[index]?' '+scales[index]:'');parts.unshift(text);} + value/=1000n;index++; + } + return parts.join(' و '); +} +function tomanWords(rials){ + const whole=rials/10n,fraction=rials%10n; + if(!fraction)return numberWords(whole)+' تومان'; + return (whole?numberWords(whole)+' و ':'')+numberWords(fraction)+' دهم تومان'; +} +function caretAfterDigits(value,count){ + if(!count)return 0; + for(let index=0;index16 || BigInt(digits)>BigInt(Number.MAX_SAFE_INTEGER)){words.textContent='';return;} + const leading=normalized.replace(separators,'').length-digits.length; + const left=Math.max(0,digitCount(raw.slice(0,start))-leading),right=Math.max(0,digitCount(raw.slice(0,end))-leading); + input.value=grouped(digits); + input.setSelectionRange(caretAfterDigits(input.value,left),caretAfterDigits(input.value,right)); + words.textContent='معادل: '+tomanWords(BigInt(digits)); +} +input.addEventListener('input',formatAmount); +input.addEventListener('compositionend',()=>formatAmount()); +input.addEventListener('beforeinput',event=>{ + const start=input.selectionStart,end=input.selectionEnd; + if(start!==end || event.isComposing)return; + const backward=event.inputType==='deleteContentBackward',forward=event.inputType==='deleteContentForward'; + const adjacent=backward?start-1:start; + if((!backward && !forward) || !/[٬,]/.test(input.value[adjacent] || ''))return; + // Deleting a separator removes the adjacent digit, so grouping cannot trap the caret. + const target=backward?adjacent-1:adjacent+1; + const count=digitCount(input.value.slice(0,target)); + const raw=input.value.slice(0,target)+input.value.slice(target+1); + event.preventDefault();input.value=ascii(raw).replace(separators,''); + input.setSelectionRange(count,count);input.dispatchEvent(new InputEvent('input',{bubbles:true,inputType:event.inputType})); +}); +input.addEventListener('blur',()=>formatAmount()); +formatAmount(); +})(); diff --git a/design-prototypes/chaharsu/receipt-banks.js b/design-prototypes/chaharsu/receipt-banks.js new file mode 100644 index 0000000..43903e5 --- /dev/null +++ b/design-prototypes/chaharsu/receipt-banks.js @@ -0,0 +1,29 @@ +/* All option labels are local text. No logos/CDN/bank APIs. + Destination fixtures are ID + display name ONLY; production supplies them from the company DB. + Origin catalog includes familiar and historical names for matching older receipts; maintain centrally. */ +(() => { +'use strict'; +const origin=document.querySelector('#origin-bank');if(!origin)return; +const banks=[ + ['melli','ملی ایران'],['mellat','ملت'],['saderat','صادرات ایران'],['sepah','سپه'],['tejarat','تجارت'], + ['refah','رفاه کارگران'],['keshavarzi','کشاورزی'],['maskan','مسکن'],['sanat-madan','صنعت و معدن'], + ['tosee-saderat','توسعه صادرات ایران'],['tosee-taavon','توسعه تعاون'],['post','پست بانک ایران'], + ['pasargad','پاسارگاد'],['parsian','پارسیان'],['saman','سامان'],['eghtesad-novin','اقتصاد نوین'], + ['karafarin','کارآفرین'],['sina','سینا'],['shahr','شهر'],['sarmayeh','سرمایه'],['dey','دی'], + ['gardeshgari','گردشگری'],['iran-zamin','ایران زمین'],['khavarmianeh','خاورمیانه'], + ['resalat','قرض‌الحسنه رسالت'],['mehr-iran','قرض‌الحسنه مهر ایران'],['iran-venezuela','مشترک ایران و ونزوئلا'], + ['melal','مؤسسه اعتباری ملل'] +]; +function option(container,id,name){container.append(new Option(name,id));} +banks.sort((left,right)=>left[1].localeCompare(right[1],'fa')).forEach(([id,name])=>option(origin,id,name)); +const historical=document.createElement('optgroup');historical.label='نام‌های قدیمی روی فیش‌ها'; +[['ayandeh','آینده'],['ansar','انصار'],['ghavamin','قوامین'],['hekmat','حکمت ایرانیان'],['mehr-eghtesad','مهر اقتصاد'],['kosar','مؤسسه کوثر'],['noor','مؤسسه نور'],['tosee','مؤسسه توسعه']].forEach(([id,name])=>option(historical,id,name)); +origin.append(historical);option(origin,'other','سایر'); +origin.addEventListener('change',()=>{ + const other=origin.value==='other';document.querySelector('#origin-other-field').hidden=!other; + document.querySelector('#origin-bank-other').disabled=!other; +}); +const destination=document.querySelector('#destination-account'); +(window.chaharsuCompanyAccounts || []).forEach(account=>option(destination,account.id,account.name)); +if(!destination.options[1])document.querySelector('#destination-help').textContent='هنوز حسابی برای انتخاب در دسترس نیست؛ با کارشناس هماهنگ کنید.'; +})(); diff --git a/design-prototypes/chaharsu/receipt-date.js b/design-prototypes/chaharsu/receipt-date.js new file mode 100644 index 0000000..b8d9e15 --- /dev/null +++ b/design-prototypes/chaharsu/receipt-date.js @@ -0,0 +1,81 @@ +/* Local Persian date selector. No libraries, CDN, network or production writes. + Use Intl's Persian calendar to determine leap Esfand; don't approximate with year % 4. + Display is Jalali; submission is Gregorian YYYY-MM-DD, date-only (no timezone timestamp). */ +(() => { +'use strict'; +const day=document.querySelector('#receipt-day'); +if(!day)return; +const month=document.querySelector('#receipt-month'); +const year=document.querySelector('#receipt-year'); +const output=document.querySelector('#receipt-date-value'); +const status=document.querySelector('#receipt-date-status'); +const todayButton=document.querySelector('#receipt-date-today'); +const monthNames=['فروردین','اردیبهشت','خرداد','تیر','مرداد','شهریور','مهر','آبان','آذر','دی','بهمن','اسفند']; +const digits=value=>String(value).replace(/\d/g,digit=>'۰۱۲۳۴۵۶۷۸۹'[digit]); +const formatter=new Intl.DateTimeFormat('en-US',{calendar:'persian',numberingSystem:'latn',timeZone:'Asia/Tehran',year:'numeric',month:'numeric',day:'numeric'}); +function parts(date){ + const values=Object.fromEntries(formatter.formatToParts(date).map(part=>[part.type,part.value])); + return {year:Number(values.year),month:Number(values.month),day:Number(values.day)}; +} +function newYear(selectedYear){ + // The native Persian calendar provides the exact first day; bounded March search. + for(let marchDay=18;marchDay<=24;marchDay++){ + const date=new Date(Date.UTC(selectedYear+621,2,marchDay,12)); + const value=parts(date); + if(value.year===selectedYear && value.month===1 && value.day===1)return date.getTime(); + } + throw new Error('Persian new year unavailable'); +} +const yearDays=new Map(); +function monthLength(selectedYear,selectedMonth){ + if(selectedMonth<=6)return 31; + if(selectedMonth<=11)return 30; + if(!yearDays.has(selectedYear))yearDays.set(selectedYear,(newYear(selectedYear+1)-newYear(selectedYear))/86400000); + return yearDays.get(selectedYear)===366?30:29; +} +function addOption(select,value,label){ + const option=document.createElement('option');option.value=String(value);option.textContent=label;select.append(option); +} +function updateDays(){ + const previous=day.value; + const length=year.value && month.value?monthLength(Number(year.value),Number(month.value)):31; + day.replaceChildren();addOption(day,'','روز'); + for(let value=1;value<=length;value++)addOption(day,value,digits(value)); + const invalid=previous && Number(previous)>length; + day.value=invalid?'':previous; + if(invalid){status.textContent='این ماه '+digits(length)+' روز دارد؛ روز پرداخت را دوباره انتخاب کنید.';output.value='';} + else updateValue(); +} +function updateValue(){ + if(!day.value || !month.value || !year.value){day.setCustomValidity('');output.value='';status.textContent='روز، ماه و سال پرداخت را کامل انتخاب کنید.';return;} + const selectedYear=Number(year.value),selectedMonth=Number(month.value),selectedDay=Number(day.value); + if(!Number.isInteger(selectedYear) || selectedMonth<1 || selectedMonth>12 || selectedDay<1 || selectedDay>monthLength(selectedYear,selectedMonth)){output.value='';day.setCustomValidity('تاریخ معتبر انتخاب کنید.');return;} + const offset=selectedMonth<=6?(selectedMonth-1)*31:186+(selectedMonth-7)*30; + // Anchor at UTC noon, add whole calendar days, serialize UTC date: no local midnight drift. + const gregorian=new Date(newYear(selectedYear)+(offset+selectedDay-1)*86400000); + const check=parts(gregorian); + if(check.year!==selectedYear || check.month!==selectedMonth || check.day!==selectedDay){ + output.value='';day.setCustomValidity('تاریخ معتبر انتخاب کنید.');status.textContent='تاریخ معتبر انتخاب کنید.';return; + } + day.setCustomValidity('');output.value=gregorian.toISOString().slice(0,10); + status.textContent=digits(day.value)+' '+monthNames[Number(month.value)-1]+' '+digits(year.value); +} +if(formatter.resolvedOptions().calendar!=='persian'){ + status.textContent='این مرورگر تقویم شمسی را پشتیبانی نمی‌کند. از نسخهٔ جدید مرورگر استفاده کنید.'; + [day,month,year,todayButton].forEach(control=>control.disabled=true);return; +} +const today=parts(new Date()); +month.replaceChildren();year.replaceChildren(); +monthNames.forEach((name,index)=>addOption(month,index+1,name)); +for(let value=today.year+1;value>=today.year-10;value--)addOption(year,value,digits(value)); +year.value=String(today.year);month.value=String(today.month);updateDays(); +document.querySelector('#receipt-form').addEventListener('receipt-date:validate',updateValue); +day.addEventListener('change',updateValue); +month.addEventListener('change',updateDays);year.addEventListener('change',updateDays); +todayButton.addEventListener('click',()=>{ + const current=parts(new Date()); + if(![...year.options].some(option=>option.value===String(current.year)))addOption(year,current.year,digits(current.year)); + year.value=String(current.year);month.value=String(current.month);day.value='';updateDays(); + day.value=String(current.day);updateValue();day.focus(); +}); +})(); diff --git a/design-prototypes/chaharsu/receipt-desktop.png b/design-prototypes/chaharsu/receipt-desktop.png new file mode 100644 index 0000000..bc01b3f Binary files /dev/null and b/design-prototypes/chaharsu/receipt-desktop.png differ diff --git a/design-prototypes/chaharsu/receipt-independent-mobile.png b/design-prototypes/chaharsu/receipt-independent-mobile.png new file mode 100644 index 0000000..b606330 Binary files /dev/null and b/design-prototypes/chaharsu/receipt-independent-mobile.png differ diff --git a/design-prototypes/chaharsu/receipt-mobile.png b/design-prototypes/chaharsu/receipt-mobile.png new file mode 100644 index 0000000..9df3cdc Binary files /dev/null and b/design-prototypes/chaharsu/receipt-mobile.png differ diff --git a/design-prototypes/chaharsu/receipt-small.png b/design-prototypes/chaharsu/receipt-small.png new file mode 100644 index 0000000..2ec86a7 Binary files /dev/null and b/design-prototypes/chaharsu/receipt-small.png differ diff --git a/design-prototypes/chaharsu/receipt-validation-mobile.png b/design-prototypes/chaharsu/receipt-validation-mobile.png new file mode 100644 index 0000000..5539f62 Binary files /dev/null and b/design-prototypes/chaharsu/receipt-validation-mobile.png differ diff --git a/design-prototypes/chaharsu/receipt-validation.js b/design-prototypes/chaharsu/receipt-validation.js new file mode 100644 index 0000000..f09c3f5 --- /dev/null +++ b/design-prototypes/chaharsu/receipt-validation.js @@ -0,0 +1,152 @@ +/* Browser-only design prototype: submit is ALWAYS prevented, nothing uploaded. + Production must validate ownership, amount, date and real file content again. + Limits here follow bootstrap IMAGE defaults, not a read of live AttachmentPolicy. */ +(() => { +'use strict'; +const form=document.querySelector('#receipt-form'); +if(!form)return; +const q=selector=>form.querySelector(selector); +const amount=q('#paid-amount'),file=q('#receipt-file'),button=q('#submit-receipt'); +const summary=q('#receipt-errors'),status=q('#receipt-validation-status'); +const groups={ + notice:{controls:[q('#notice-select')],error:q('#notice-error')}, + amount:{controls:[amount],error:q('#amount-error')}, + destination:{controls:[q('#destination-account')],error:q('#destination-error')}, + date:{controls:[q('#receipt-day'),q('#receipt-month'),q('#receipt-year')],error:q('#date-error')}, + file:{controls:[file],error:q('#file-error')} +}; +const messages=new Map(); +const touched=new Set(); +const imageChecks=new WeakMap(); +let submitted=false,busy=false,revision=0,fileRevision=0; +function focusField(control){ + const visible=control===file?q('.file-picker-label'):control; + visible.scrollIntoView({block:'center',behavior:matchMedia('(prefers-reduced-motion: reduce)').matches?'auto':'smooth'}); + control.focus({preventScroll:true}); +} +function firstControl(key){ + const controls=groups[key].controls; + return controls.find(control=>!control.value) || controls[0]; +} +function renderSummary(){ + const list=summary.querySelector('ul');list.replaceChildren(); + messages.forEach((message,key)=>{ + const item=document.createElement('li'),link=document.createElement('a'); + link.href='#'+firstControl(key).id;link.textContent=message; + link.addEventListener('click',event=>{event.preventDefault();focusField(firstControl(key));}); + item.append(link);list.append(item); + }); + summary.hidden=!submitted || messages.size===0; +} +function showError(key,message){ + const group=groups[key];group.error.textContent=message;group.error.hidden=!message; + group.controls.forEach(control=>control.setAttribute('aria-invalid',String(Boolean(message)))); + if(message)messages.set(key,message);else messages.delete(key); + renderSummary(); +} +function normalizeDigits(value){ + return value.replace(/[۰-۹]/g,digit=>String(digit.charCodeAt(0)-1776)).replace(/[٠-٩]/g,digit=>String(digit.charCodeAt(0)-1632)).trim(); +} +function amountError(){ + q('#paid-amount-value').value=''; + const value=normalizeDigits(amount.value); + if(!value)return 'مبلغ واریز را به ریال وارد کنید.'; + if(value.startsWith('-'))return 'مبلغ باید بیشتر از صفر باشد.'; + if(/[.٫]/.test(value))return 'مبلغ را به ریال و بدون اعشار وارد کنید.'; + if(!/^\d+$/.test(value) && !/^\d{1,3}(?:[,٬ \u00a0]\d{3})+$/.test(value))return 'مبلغ را فقط با عدد بنویسید؛ مثلاً ۲۸۰۰۰۰۰۰۰.'; + const digits=value.replace(/[,٬ \u00a0]/g,'').replace(/^0+(?=\d)/,''); + if(digits==='0')return 'مبلغ باید بیشتر از صفر باشد.'; + // Avoid imprecise conversion: payload remains an integer string. + if(digits.length>16 || BigInt(digits)>BigInt(Number.MAX_SAFE_INTEGER))return 'مبلغ بسیار بزرگ است؛ عدد روی فیش را دوباره بررسی کنید.'; + q('#paid-amount-value').value=digits;return ''; +} +function noticeError(){ + if(q('input[name="receipt-mode"]:checked').value!=='linked')return ''; + const selected=q('#notice-select').value; + if(!(window.chaharsuPaymentNotices || []).some(notice=>notice.id===selected))return 'واریز مرتبط را انتخاب کنید؛ اگر اعلامیه ندارید، واریز مستقل را بزنید.'; + return ''; +} +function destinationError(){ + const selected=q('#destination-account').value; + if(!(window.chaharsuCompanyAccounts || []).some(account=>account.id===selected))return 'حساب مقصد را انتخاب کنید؛ مبلغ را به کدام حساب شرکت واریز کرده‌اید؟'; + return ''; +} +function dateError(){ + form.dispatchEvent(new Event('receipt-date:validate')); + const controls=groups.date.controls; + if(controls.some(control=>!control.value))return 'تاریخ را کامل کنید؛ روز، ماه و سال لازم‌اند.'; + if(controls.some(control=>control.disabled || !control.checkValidity()))return 'تاریخ معتبر انتخاب کنید؛ روز باید در ماه و سال انتخاب‌شده وجود داشته باشد.'; + const value=q('#receipt-date-value').value; + if(!/^\d{4}-\d{2}-\d{2}$/.test(value))return 'تاریخ معتبر انتخاب کنید؛ روز، ماه و سال را دوباره بررسی کنید.'; + const todayParts=new Intl.DateTimeFormat('en-US',{timeZone:'Asia/Tehran',year:'numeric',month:'2-digit',day:'2-digit'}).formatToParts(new Date()); + const today=Object.fromEntries(todayParts.map(part=>[part.type,part.value])); + if(value>[today.year,today.month,today.day].join('-'))return 'تاریخ واریز نمی‌تواند بعد از امروز باشد.'; + return ''; +} +function detectedType(bytes){ + const text=(start,end)=>String.fromCharCode(...bytes.slice(start,end)); + if(bytes[0]===255 && bytes[1]===216 && bytes[2]===255)return 'image/jpeg'; + if([...bytes.slice(0,8)].join(',')==='137,80,78,71,13,10,26,10')return 'image/png'; + if(['GIF87a','GIF89a'].includes(text(0,6)))return 'image/gif'; + if(text(0,4)==='RIFF' && text(8,12)==='WEBP')return 'image/webp'; + return ''; +} +async function checkImage(selected){ + const max=Number(file.dataset.maxBytes); + if(!selected.size)return 'تصویر انتخاب‌شده خالی است؛ تصویر دیگری انتخاب کنید.'; + if(selected.size>max)return 'حجم تصویر بیشتر از ۱۰ مگابایت است؛ تصویر کم‌حجم‌تری انتخاب کنید.'; + const allowed={jpg:'image/jpeg',jpeg:'image/jpeg',png:'image/png',gif:'image/gif',webp:'image/webp'}; + const extension=selected.name.split('.').pop().toLowerCase(); + if(!allowed[extension])return 'فقط تصویر JPG، PNG، GIF یا WEBP انتخاب کنید.'; + const type=detectedType(new Uint8Array(await selected.slice(0,16).arrayBuffer())); + if(type!==allowed[extension] || (selected.type && selected.type!==type))return 'محتوای فایل با نوع تصویر سازگار نیست؛ تصویر اصلی فیش را انتخاب کنید.'; + const readable=await new Promise(resolve=>{ + const image=new Image(),url=URL.createObjectURL(selected); + image.onload=()=>{URL.revokeObjectURL(url);resolve(image.naturalWidth>0 && image.naturalHeight>0);}; + image.onerror=()=>{URL.revokeObjectURL(url);resolve(false);};image.src=url; + }); + return readable?'':'تصویر باز نمی‌شود یا خراب است؛ تصویر دیگری انتخاب کنید.'; +} +async function fileError(){ + const selected=file.files[0]; + if(!selected)return 'تصویر فیش واریز را انتخاب کنید.'; + if(!imageChecks.has(selected))imageChecks.set(selected,checkImage(selected).catch(()=> 'تصویر قابل بررسی نیست؛ دوباره آن را انتخاب کنید.')); + return imageChecks.get(selected); +} +const validators={notice:noticeError,amount:amountError,destination:destinationError,date:dateError,file:fileError}; +function refresh(key){ + status.textContent=''; + if(key==='file')return refreshFile(); + if(submitted || touched.has(key))showError(key,validators[key]()); +} +async function refreshFile(){ + const token=++fileRevision;touched.add('file'); + const message=await fileError();if(token===fileRevision)showError('file',message); +} +form.addEventListener('input',()=>{revision++;status.textContent='';}); +form.addEventListener('change',()=>{revision++;status.textContent='';}); +amount.addEventListener('blur',()=>{touched.add('amount');refresh('amount');}); +amount.addEventListener('input',()=>{q('#paid-amount-value').value='';refresh('amount');}); +groups.date.controls.forEach(control=>control.addEventListener('change',()=>refresh('date'))); +q('#receipt-date-today').addEventListener('click',()=>{revision++;refresh('date');}); +q('#notice-select').addEventListener('change',()=>refresh('notice')); +q('#destination-account').addEventListener('change',()=>refresh('destination')); +q('#destination-account').addEventListener('blur',()=>{touched.add('destination');refresh('destination');}); +form.querySelectorAll('input[name="receipt-mode"]').forEach(control=>control.addEventListener('change',()=>refresh('notice'))); +file.addEventListener('change',()=>refresh('file')); +q('#clear-file').addEventListener('click',()=>{revision++;refresh('file');}); +form.addEventListener('submit',async event=>{ + event.preventDefault();if(busy)return; + submitted=true;busy=true;button.disabled=true;button.textContent='در حال بررسی…';status.textContent=''; + const current=revision; + try{ + const results=[['notice',noticeError()],['amount',amountError()],['destination',destinationError()],['date',dateError()],['file',await fileError()]]; + if(current!==revision){status.textContent='اطلاعات فرم تغییر کرده است؛ دوباره دکمهٔ بررسی را بزنید.';return;} + messages.clear();results.forEach(([key,message])=>showError(key,message)); + if(messages.size){focusField(firstControl(messages.keys().next().value));return;} + status.textContent='اطلاعات فرم کامل و معتبر است. این پیش‌نمایش است؛ فیش ارسال یا ذخیره نشده است.'; + }finally{busy=false;button.disabled=false;button.textContent='بررسی و ادامه';} +}); +// Enable only when prevention and validation handlers are installed; no accidental native submit. +button.disabled=false; +})(); diff --git a/design-prototypes/chaharsu/requests-desktop.png b/design-prototypes/chaharsu/requests-desktop.png new file mode 100644 index 0000000..1069b64 Binary files /dev/null and b/design-prototypes/chaharsu/requests-desktop.png differ diff --git a/design-prototypes/chaharsu/requests-mobile.png b/design-prototypes/chaharsu/requests-mobile.png new file mode 100644 index 0000000..a509487 Binary files /dev/null and b/design-prototypes/chaharsu/requests-mobile.png differ diff --git a/design-prototypes/chaharsu/requests-small.png b/design-prototypes/chaharsu/requests-small.png new file mode 100644 index 0000000..9655f1a Binary files /dev/null and b/design-prototypes/chaharsu/requests-small.png differ diff --git a/design-prototypes/chaharsu/service-pages.css b/design-prototypes/chaharsu/service-pages.css new file mode 100644 index 0000000..7d0240a --- /dev/null +++ b/design-prototypes/chaharsu/service-pages.css @@ -0,0 +1,256 @@ +/* Independent design prototype: never edit production styles for these pages. + Match home typography: main text/actions >=14px, secondary labels 12px. + Keep text readable on small screens; compact spacing instead of shrinking text. */ +@font-face{font-family:ChaharsuVazirmatn;src:url('../../static/fonts/vazirmatn-arabic-wght-normal.woff2?v=1791111429240969400') format('woff2');font-weight:100 900;font-display:swap} +@font-face{font-family:ChaharsuLatin;src:url('../../static/fonts/vazirmatn-latin-wght-normal.woff2?v=1791111429240969400') format('woff2');font-weight:100 900;font-display:swap} +body{font-family:ChaharsuVazirmatn,ChaharsuLatin,sans-serif;color:var(--color-ink);font-size:14px} +[hidden]{display:none!important} +a,button,input{touch-action:manipulation} +a:focus-visible,button:focus-visible,input:focus-visible{outline:3px solid var(--color-sea);outline-offset:4px} +.icon{width:23px;height:23px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex-shrink:0} +.preview-bar{background:var(--color-mint);text-align:center;padding:7px 14px;font-size:12px;line-height:1.65;border-bottom:1px solid #cde6f7} +.page-header{border-bottom:1px solid #dce2e8;background:#eeeeee} +.header-inner{max-width:1120px;margin:auto;min-height:76px;display:flex;align-items:center;justify-content:space-between;padding:0 24px;gap:16px} +.brand{display:flex;align-items:center;gap:14px;color:var(--color-ink)} +.brand img{width:85px;height:auto} +.brand-copy{border-right:1px solid #cdd7e0;padding-right:14px} +.brand b{display:block;font-size:20px;font-weight:900;line-height:1.3} +.brand small{display:block;font-size:12px;color:#526c86;margin-top:4px} +.header-home{font-size:14px;font-weight:800;color:#1977ce;display:flex;align-items:center;min-height:44px} +.service-main{width:100%;max-width:900px;margin:auto;padding:24px 24px 40px} +.breadcrumb{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:12px;color:#526c86;margin-bottom:16px} +.breadcrumb a{color:#1977ce;min-height:44px;display:flex;align-items:center} +.title-row{display:flex;align-items:flex-start;gap:14px} +.page-icon{display:grid;place-items:center;width:48px;height:48px;flex-shrink:0;border-radius:15px;background:var(--color-mint);color:#1977ce} +.service-main h1{font-size:28px;line-height:1.5;font-weight:900;letter-spacing:0;margin:0} +.subtitle{font-size:14px;line-height:1.7;color:#526c86;margin:7px 0 0} +.soon{display:inline-block;color:#805300;background:#fff3da;border:1px solid #f3ddae;border-radius:20px;padding:3px 9px;font-size:12px;font-weight:700;white-space:nowrap} +.service-notice{display:flex;align-items:center;gap:10px;padding:12px 16px;margin:18px 0;border:1px solid #f3ddae;border-radius:15px;background:#fff8eb} +.service-notice p{font-size:14px;line-height:1.65;color:#805300;margin:0;flex:1;min-width:0} +.content-card{border:1px solid #dce5ed;border-radius:23px;padding:22px;background:white;margin:16px 0} +.content-card h2{font-size:20px;font-weight:900;line-height:1.6;margin:0 0 8px} +.content-card p{font-size:14px;line-height:1.7;color:#526c86;margin:0 0 16px} +.upload-zone{display:flex;flex-direction:column;align-items:center;gap:10px;border:1px dashed #91bfdd;border-radius:18px;padding:22px 18px;background:#f6fbff} +.upload-zone svg{color:#1977ce;width:32px;height:32px} +.upload-zone label{margin:0;font-size:16px;font-weight:800;line-height:1.6} +.upload-zone span{font-size:14px;color:#526c86;text-align:center;line-height:1.65} +.upload-zone input[type=file]{width:100%;min-width:0;max-width:390px;margin:0;font-size:14px;background:white;padding:8px;border:1px solid #dce5ed;border-radius:12px;min-height:44px} +.upload-zone input::file-selector-button{font:inherit;min-height:30px} +.file-selection{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;border:1px solid #cde6f7;border-radius:12px;background:var(--color-mint);margin-top:12px} +.file-selection p{margin:0;min-width:0;overflow-wrap:anywhere;color:var(--color-ink);font-size:14px} +.file-selection button{background:transparent;box-shadow:none;min-height:44px;padding:6px 8px;color:#1977ce;font-size:14px;flex-shrink:0} +.file-selection button:hover{transform:none;box-shadow:none;color:var(--color-ink);background:white} +.receipt-actions{display:flex;justify-content:space-between;align-items:center;gap:12px;margin-top:16px;flex-wrap:wrap} +.receipt-actions button{background:var(--color-ink);font-size:14px;min-height:44px} +.receipt-actions a{color:#1977ce;font-size:14px;font-weight:700;min-height:44px;display:flex;align-items:center} +.account-fields{display:grid;gap:0;margin:12px 0 0} +.account-fields div{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0;border-top:1px solid #e7edf2;font-size:14px;line-height:1.7} +.account-fields dt{color:#526c86} +.account-fields dd{margin:0;color:var(--color-ink);font-weight:700;text-align:left;overflow-wrap:anywhere;min-width:0} +.identifier-card{background:#f6fbff;border-color:#cde6f7} +.service-footer{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:18px 0 0;font-size:14px;line-height:1.7} +.service-footer a{color:#1977ce;font-weight:800;min-height:44px;display:flex;align-items:center} +.page-footer{font-size:12px;line-height:1.8;color:#526c86;text-align:center;margin:24px 0 0} +.page-footer a{color:inherit;white-space:nowrap} +.page-footer a:hover{color:#1977ce} +@media(max-width:639px){ +.preview-bar{padding:5px 12px} +.header-inner{min-height:65px;padding:0 12px;gap:8px} +.brand{gap:8px}.brand img{width:56px}.brand-copy{padding-right:8px}.brand b{font-size:18px} +.brand small{font-size:12px} +.header-home{font-size:14px} +.service-main{padding:10px 12px 28px} +.breadcrumb{margin-bottom:8px} +.title-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:0 10px} +.title-row>.page-icon{grid-column:2;grid-row:1} +.title-row>div{display:contents} +.service-main h1{grid-column:1;grid-row:1;font-size:24px;align-self:center} +.subtitle{grid-column:1/-1;margin-top:6px} +.page-icon{width:36px;height:36px;border-radius:12px} +.service-notice{margin:14px 0;padding:10px 12px;gap:8px} +.content-card{padding:16px;border-radius:20px;margin:12px 0} +.content-card h2{font-size:18px;margin-bottom:6px} +.content-card p{margin-bottom:12px} +.upload-zone{padding:14px 12px;gap:8px} +.upload-zone>svg{display:none} +.receipt-actions{flex-direction:column;align-items:stretch;gap:6px;margin-top:12px} +.receipt-actions button{width:100%} +.receipt-actions a{justify-content:center} +.account-fields{margin-top:8px} +.account-fields div{padding:9px 0;gap:8px} +.service-footer{align-items:stretch;flex-direction:column;gap:4px;margin-top:12px} +.page-footer{margin-top:18px} +} +@media(max-width:359px){.brand img{width:44px}.brand small{font-size:12px}.header-home{font-size:14px}.content-card{padding:14px}} +@media(prefers-reduced-motion:reduce){*{scroll-behavior:auto!important;transition:none!important}} + +/* Payment notice & receipt flow. All values belong to a single customer notice. + Use rem, 44px touch targets, dark text on pale surfaces; local SVG/fonts only. */ +a,button,input,textarea,select,summary{touch-action:manipulation} +textarea:focus-visible,select:focus-visible{outline:3px solid var(--color-ink-soft);outline-offset:3px} +.header-home,.breadcrumb a,.service-footer a,.receipt-actions a,.file-selection button{color:var(--color-ink)} +.page-icon{color:var(--color-ink-soft)} +.finance-button,.copy-field,.notice-toggle{font-family:inherit;box-shadow:none;transform:none!important;letter-spacing:0} +.finance-button{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:10px 16px;border:1px solid var(--color-ink);border-radius:12px;background:var(--color-ink);color:white;font-size:.875rem;font-weight:800;line-height:1.7;text-align:center} +.finance-button:hover{background:var(--color-ink-soft);color:white;box-shadow:none} +.finance-button.secondary{background:white;color:var(--color-ink);border-color:#bfcede} +.finance-button.secondary:hover{background:var(--color-mint);border-color:var(--color-ink-soft)} +.list-heading{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:24px 0 12px} +.list-heading h2{font-size:1.25rem;font-weight:900;line-height:1.6;margin:0} +.list-heading p{font-size:.875rem;color:#526c86;line-height:1.7;margin:4px 0 0} +.meta{font-size:.75rem;color:#526c86;flex-shrink:0} +.finance-search{display:flex;align-items:center;gap:10px;background:white;border:1px solid #bfcede;padding:0 12px;border-radius:12px;margin-bottom:16px;color:#526c86} +.finance-search input{border:0!important;background:transparent!important;box-shadow:none!important;border-radius:10px;padding:10px 0;min-height:44px;min-width:0;font-size:1rem!important;color:var(--color-ink);width:100%} +.finance-search:focus-within{outline:3px solid var(--color-ink-soft);outline-offset:3px} +.finance-search input:focus{outline:none} +.finance-search input::placeholder{color:#526c86;opacity:1} +.payment-notices{display:grid;gap:12px} +.payment-notice{min-width:0;border:1px solid #dce5ed;border-radius:20px;background:white;overflow:hidden} +.payment-notice:has([aria-expanded=true]){border-color:#91b8d7} +.payment-notice h3{margin:0} +.notice-toggle{width:100%;display:flex;align-items:center;justify-content:space-between;gap:16px;padding:20px;background:white;color:var(--color-ink);text-align:right;border-radius:20px;min-height:44px} +.notice-toggle:hover{background:#f6fbff;color:var(--color-ink);box-shadow:none} +.notice-toggle:focus-visible{outline-offset:-4px;outline-color:var(--color-ink-soft)} +.notice-toggle[aria-expanded=true]>.icon{transform:rotate(180deg)} +.notice-summary{display:grid;gap:8px;min-width:0;flex:1} +.notice-top{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px} +.notice-title{font-size:1rem;font-weight:800;line-height:1.7;overflow-wrap:anywhere} +.notice-amount{font-size:1.25rem;line-height:1.6;font-weight:900} +.notice-meta{font-size:.75rem;line-height:1.8;color:#526c86;font-weight:400;overflow-wrap:anywhere} +.payment-state{font-size:.75rem;font-weight:700;line-height:1.7;background:#fff3da;color:#805300;padding:3px 9px;border-radius:20px;white-space:nowrap} +.payment-state.received{background:var(--color-mint);color:var(--color-ink)} +.notice-detail{padding:0 20px 20px;border-top:1px solid #e7edf2} +.notice-context{display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:14px 0;color:#526c86;font-size:.75rem;line-height:1.8} +.payment-fields{margin:0} +.payment-field{display:grid;grid-template-columns:140px minmax(0,1fr) 64px;align-items:center;gap:12px;padding:10px 0;border-top:1px solid #e7edf2;font-size:.875rem;line-height:1.7;min-width:0} +.payment-field dt{color:#526c86;font-weight:400;margin:0;overflow-wrap:anywhere} +.payment-field dd{color:var(--color-ink);font-weight:700;margin:0;min-width:0;overflow-wrap:anywhere;white-space:pre-wrap} +.payment-field dd.numeric-value{text-align:right;unicode-bidi:isolate;font-size:1rem} +.payment-field.no-copy{grid-template-columns:140px minmax(0,1fr)} +.copy-field{padding:6px 10px;min-height:44px;min-width:44px;font-size:.875rem;font-weight:700;line-height:1.5;color:var(--color-ink);border:1px solid #dce5ed;background:#f6fbff;border-radius:10px} +.copy-field:hover{background:var(--color-mint);color:var(--color-ink);border-color:#91b8d7;box-shadow:none} +.notice-actions{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-top:16px} +.copy-help{font-size:.875rem;color:#526c86;margin:12px 0 0;line-height:1.7} +.empty-notices{text-align:center;padding:24px 16px;background:white;border:1px solid #dce5ed;border-radius:20px} +.empty-notices h3{font-size:1.125rem;font-weight:800;margin:0 0 8px} +.empty-notices p{font-size:.875rem;line-height:1.7;color:#526c86;margin:0 0 16px} +.copy-feedback:empty{display:none} +.copy-feedback{position:fixed;bottom:max(16px,env(safe-area-inset-bottom));left:50%;transform:translateX(-50%);width:max-content;max-width:calc(100% - 24px);z-index:50;border-radius:12px;background:var(--color-ink);color:white;padding:12px 16px;font-size:.875rem;line-height:1.7;box-shadow:0 8px 28px #08376b26;pointer-events:none} +.manual-copy{width:calc(100% - 24px);max-width:560px;border:1px solid #dce5ed;padding:24px;border-radius:20px;background:white;color:var(--color-ink)} +.manual-copy::backdrop{background:#08376b80} +.manual-copy h2{font-size:1.25rem;line-height:1.6;font-weight:900;margin:0 0 12px} +.manual-copy p{font-size:.875rem;line-height:1.7;color:#526c86;margin:0 0 16px} +.manual-copy textarea{width:100%;min-height:240px;font-size:1rem;line-height:1.7;padding:12px} +.manual-copy>div{display:flex;flex-wrap:wrap;gap:12px;margin-top:16px} +.receipt-mode{display:grid;grid-template-columns:1fr 1fr;gap:12px;border:0;padding:0;margin:16px 0} +.receipt-mode label{display:flex;align-items:center;gap:10px;margin:0;padding:12px;min-height:64px;border:1px solid #bfcede;border-radius:12px;cursor:pointer;background:white;color:var(--color-ink)} +.receipt-mode label:has(input:checked){background:var(--color-mint);border-color:var(--color-ink-soft)} +.receipt-mode input{width:20px;height:20px;flex-shrink:0;accent-color:var(--color-ink)} +.receipt-mode b{display:block;font-size:.875rem;line-height:1.7;font-weight:800} +.receipt-mode small{display:block;font-size:.75rem;line-height:1.8;color:#526c86;font-weight:400;margin-top:2px} +#receipt-form .field-label{display:block;font-size:1rem;font-weight:700;color:var(--color-ink);margin:12px 0 8px;line-height:1.7} +#receipt-form .field-label span{font-size:.875rem;font-weight:400;color:#526c86} +#receipt-form input:not([type=radio]),#receipt-form select,#receipt-form textarea{font-size:1rem;line-height:1.7;min-height:44px;min-width:0;padding:10px 12px;border:1px solid #bfcede;border-radius:12px;box-shadow:none;width:100%;color:var(--color-ink)} +#receipt-form input::placeholder,#receipt-form textarea::placeholder{color:#526c86;opacity:1} +#receipt-form .field-help{font-size:.875rem;color:#526c86;line-height:1.7;margin:8px 0 0} +#receipt-form .inline-error{background:#fff8eb;border:1px solid #f3ddae;border-radius:12px;padding:12px;color:#805300;font-size:.875rem;line-height:1.7;margin:12px 0} +.selected-payment{background:#f6fbff;border:1px solid #cde6f7;border-radius:16px;padding:16px;margin-top:16px} +.selected-payment h3{font-size:1rem;line-height:1.7;font-weight:800;margin:0 0 8px} +.compact-fields .payment-field{padding:8px 0} +.text-action{display:flex;align-items:center;min-height:44px;font-size:.875rem;font-weight:700;color:var(--color-ink);margin-top:8px;line-height:1.7} +.receipt-field-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px} +.receipt-field-grid>div{min-width:0} +#receipt-form .upload-zone{margin:20px 0 0} +#receipt-form .upload-zone label{color:var(--color-ink);font-size:1rem} +#receipt-form .upload-zone input{max-width:100%;background:white} +#receipt-form .upload-zone input::file-selector-button{min-height:44px;background:var(--color-ink);color:white;border:0;border-radius:8px;padding:8px 12px;font:inherit;margin-inline-end:8px;cursor:pointer} +#receipt-form textarea{min-height:100px;resize:vertical} +.receipt-summary{padding:12px;background:var(--color-mint);border-radius:12px;margin-top:16px;font-size:.875rem;line-height:1.7;color:var(--color-ink);overflow-wrap:anywhere} +@media(max-width:639px){ +.list-heading{margin-top:20px;gap:8px}.list-heading h2{font-size:1.125rem} +.finance-search{gap:8px;padding:0 10px} +.notice-toggle{padding:16px 14px;gap:8px}.notice-summary{gap:6px}.notice-title{font-size:.875rem}.notice-amount{font-size:1.125rem} +.notice-detail{padding:0 14px 16px}.notice-context{padding:12px 0} +.payment-field,.payment-field.no-copy{grid-template-columns:minmax(0,1fr) 56px;gap:6px 8px;padding:12px 0} +.payment-field dt{grid-column:1;grid-row:1}.payment-field dd{grid-column:1;grid-row:2} +.payment-field .copy-field{grid-column:2;grid-row:1/3;align-self:center} +.payment-field.no-copy dd{grid-column:1/-1} +.payment-field dd.numeric-value{font-size:.875rem} +.notice-actions{grid-template-columns:1fr;gap:8px}.notice-actions .receipt-link{grid-row:1} +.copy-help{margin-top:8px}.receipt-mode{grid-template-columns:1fr;gap:8px;margin:12px 0} +.receipt-mode label{min-height:64px;padding:10px 12px} +.receipt-field-grid{grid-template-columns:1fr;gap:0}.selected-payment{padding:12px} +.manual-copy{padding:16px}#receipt-form .upload-zone{margin-top:16px} +} +#receipt-form .upload-zone .file-picker-label{display:inline-flex;align-items:center;justify-content:center;min-height:44px;padding:10px 20px;margin:0;border-radius:12px;background:var(--color-ink);color:white;cursor:pointer;font-size:.875rem;line-height:1.7} +#receipt-form .upload-zone:has(input:focus-visible) .file-picker-label{outline:3px solid var(--color-ink-soft);outline-offset:4px} +#receipt-form input#receipt-file.sr-only{position:absolute;width:1px;height:1px;min-height:0;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} +@media(max-width:639px){ +/* Bank identifiers need the whole mobile row: avoid an isolated final IBAN digit. */ +.payment-field:has(dd.numeric-value) dd{grid-column:1/-1} +.payment-field:has(dd.numeric-value) .copy-field{grid-row:1} +} + +/* Compact financial UI: keep supplementary prose compact and visible, keep payment values readable. */ +.finance-guidance,#receipt-form .finance-guidance{margin:16px 0;padding:10px 12px;border-radius:12px;background:var(--color-mint);color:var(--color-ink);font-size:.875rem;line-height:1.7;font-weight:400} +.notice-bank{display:flex;align-items:baseline;justify-content:space-between;flex-wrap:wrap;gap:4px 16px;padding:12px 0;font-size:.875rem;line-height:1.7} +.notice-bank-name{font-weight:800}.notice-holder{color:#526c86;font-weight:400} +.essential-fields{display:grid;grid-template-columns:1fr 1fr;column-gap:24px} +.essential-fields .payment-field{grid-template-columns:minmax(0,1fr) 56px;gap:0 8px;padding:8px 0} +.essential-fields .payment-field dt{grid-column:1;grid-row:1} +.essential-fields .payment-field dd{grid-column:1/-1;grid-row:2} +.essential-fields .copy-field{grid-column:2;grid-row:1;align-self:center} +.notice-extra{margin-top:8px;border-top:1px solid #e7edf2} +.extra-fields .payment-field{grid-template-columns:140px minmax(0,1fr);padding:8px 0} +.extra-fields .payment-field dd{font-weight:400;line-height:1.8} +.notice-extra .notice-context{padding:4px 0 8px} +.recommended-mode{border-inline-start:3px solid var(--color-ink-soft)!important} +.recommended-badge{display:inline-block;margin-inline-start:6px;padding:1px 7px;font-size:.75rem;font-weight:700;line-height:1.8;border-radius:20px;background:var(--color-ink);color:white} +@media(max-width:639px){ +.essential-fields{grid-template-columns:1fr;gap:0} +.essential-fields .payment-field{padding:6px 0;gap:0 8px} +.essential-fields .payment-field dd{margin-top:-2px} +.extra-fields .payment-field{grid-template-columns:1fr;gap:4px} +.extra-fields .payment-field dt,.extra-fields .payment-field dd{grid-column:1} +.notice-bank{display:grid;gap:2px;padding:10px 0} +.finance-guidance,#receipt-form .finance-guidance{margin:12px 0} +} + +/* Supplementary notice information is permanent within the opened notice card. */ +.notice-extra h4{font-size:.875rem;font-weight:800;line-height:1.7;margin:12px 0 8px;color:var(--color-ink)} +/* Native Persian day/month/year controls: readable labels and 44px targets. */ +.receipt-date{border:0;padding:0;margin:20px 0 0;min-width:0} +.receipt-date legend{font-size:1rem;font-weight:700;line-height:1.7;color:var(--color-ink);padding:0;margin-bottom:8px} +.receipt-date legend span{font-size:.875rem;font-weight:400;color:#526c86} +.date-selects{display:grid;grid-template-columns:minmax(64px,.7fr) minmax(110px,1.2fr) minmax(80px,1fr);gap:12px} +.date-selects>div{min-width:0} +#receipt-form .date-selects label{font-size:.875rem;font-weight:400;line-height:1.7;color:#526c86;margin:0 0 4px} +#receipt-form .date-selects select{padding:10px 8px;font-size:1rem;min-height:44px;width:100%;text-align:right} +.date-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-top:8px} +#receipt-form .date-tools p{font-size:.875rem;line-height:1.7;color:#526c86;margin:0} +.date-tools button{flex-shrink:0;padding:6px 12px} +#receipt-form #receipt-date-status{font-size:.875rem;line-height:1.7;color:var(--color-ink);margin:4px 0 0;min-height:24px} +@media(max-width:639px){ +.date-selects{grid-template-columns:minmax(0,.65fr) minmax(0,1.25fr) minmax(0,1fr);gap:8px} +#receipt-form .date-selects select{padding:10px 4px} +.receipt-date{margin-top:16px} +.date-tools{gap:8px} +} +/* Minimal Today action, preserving a 44px touch target and visible keyboard focus. */ +#receipt-date-today{min-height:44px;min-width:44px;padding:4px 8px;border:0;background:transparent;color:var(--color-ink);box-shadow:none;font-size:.875rem;font-weight:700} +#receipt-date-today:hover{background:var(--color-mint);color:var(--color-ink)} +/* Explicit, readable field errors and a linked summary; never color alone. */ +#receipt-form .field-error{font-size:.875rem;line-height:1.8;font-weight:500;color:#9f1f1f;margin:8px 0 0} +#receipt-form [aria-invalid=true]:not([type=hidden]):not([type=file]){border-color:#b93333;background:#fffafa} +#receipt-form .upload-zone:has(input[aria-invalid=true]){border-color:#b93333;background:#fffafa} +.receipt-errors{background:#fffafa;border:1px solid #e5aaaa;border-radius:16px;padding:16px;margin:16px 0;color:#9f1f1f} +#receipt-form .receipt-errors h2{font-size:1rem;font-weight:800;line-height:1.7;margin:0 0 8px;color:#9f1f1f} +.receipt-errors ul{padding:0;list-style:none;margin:0} +.receipt-errors a{display:flex;align-items:center;min-height:44px;font-size:.875rem;line-height:1.7;color:#9f1f1f;text-decoration:underline;text-underline-offset:3px} +#receipt-form .validation-status{font-size:.875rem;line-height:1.8;color:var(--color-ink);margin:12px 0 0} +#receipt-form .validation-status:empty{display:none} +#receipt-form .file-picker-label span{font-size:.875rem;color:inherit} +#receipt-form .amount-words{font-size:.875rem;line-height:1.8;font-weight:700;color:var(--color-ink);margin:8px 0 0;overflow-wrap:anywhere} +#receipt-form .amount-words:empty{display:none} +#paid-amount{text-align:right;font-variant-numeric:tabular-nums} +.bank-fields{margin-top:12px}