fix UI UX

This commit is contained in:
javad committed 2026-10-06 12:08:16 +03:30
1 parent 1cbbbbd0dc
commit d99e47ecce
11 files changed
+333 -32

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
/* Shared Chaharsu navigation. Composer changes are confined to the dock. */
.customer-mobile-dock.chaharsu-dock {
grid-template-columns: repeat(3, minmax(0, 1fr));
bottom: calc(12px + env(safe-area-inset-bottom));
}
.customer-mobile-dock.chaharsu-dock a {
min-height: 52px;
gap: 4px;
font-size: 12px;
color: #cadbea;
}
.customer-mobile-dock.chaharsu-dock svg {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.customer-mobile-dock.chaharsu-dock a.active {
background: #0b4c8c;
color: #fff;
}
.customer-mobile-dock.chaharsu-dock a:focus-visible {
outline: 3px solid #fff;
outline-offset: -3px;
}
+36
View File
@@ -0,0 +1,36 @@
/* Chaharsu profile: scoped rules preserve the ticket composer and other forms. */
.profile-breadcrumb { display: inline-flex; align-items: center; min-height: 44px; }
.profile-heading { display: flex; align-items: flex-start; gap: 16px; margin-top: 8px; }
.profile-heading .service-icon { flex-shrink: 0; }
.profile-heading h1 { margin: 0; }
.customer-profile .profile-heading p { margin: 6px 0 0; }
.customer-profile-form.pay-form { max-width: none; margin: 0; padding: 0; border: 0; background: transparent; box-shadow: none; }
.customer-profile-form .pay-card { margin: 0 0 20px; padding: 22px; border-radius: 22px; }
.customer-profile-form h2 { margin: 0 0 12px; }
.profile-card-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
.profile-card-heading h2 { margin: 0; }
.profile-mobile { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; color: #526c86; font-size: .875rem; }
.profile-mobile bdi { color: #08376b; font-weight: 700; }
.profile-field-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 20px 24px; }
.customer-profile-form .pay-field { margin: 0; min-width: 0; }
.customer-profile-form .pay-field label { font-size: .875rem; }
.customer-profile-form input:not([type=hidden]),.customer-profile-form select { min-height: 48px; padding: 10px 12px; font-size: 1rem; box-shadow: none; }
.customer-profile-form input[type=password] { direction: ltr; text-align: right; }
.customer-profile .profile-guidance { margin: 8px 0 16px; color: #526c86; font-size: .875rem; line-height: 1.8; }
.customer-profile [data-location-hint] { margin: 12px 0 0; }
.customer-profile .is-error,.customer-profile .errorlist { color: #9f1f1f; font-size: .875rem; line-height: 1.8; }
.customer-profile .errorlist { list-style: none; padding: 0; margin: 6px 0 0; }
.customer-profile-form [aria-invalid=true] { border-color: #9f1f1f; }
.profile-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
.profile-actions .pay-button { min-width: 160px; }
@media (max-width: 639px) {
.profile-heading { display: grid; grid-template-columns: minmax(0, 1fr) 40px; gap: 8px 12px; align-items: center; }
.profile-heading > div { display: contents; }
.profile-heading h1 { grid-column: 1; grid-row: 1; }
.profile-heading .service-icon { grid-column: 2; grid-row: 1; width: 40px; height: 40px; border-radius: 12px; }
.customer-profile .profile-heading p { grid-column: 1 / -1; grid-row: 2; margin: 0; }
.customer-profile-form .pay-card { padding: 16px; border-radius: 20px; margin-bottom: 16px; }
.profile-field-grid { grid-template-columns: minmax(0, 1fr); gap: 18px; }
.profile-actions { flex-direction: column; gap: 8px; margin-top: 20px; }
.profile-actions .pay-button { width: 100%; }
}
+42
View File
@@ -0,0 +1,42 @@
/* Ticket history follows the Chaharsu shell; no composer selectors. */
.customer-tickets .chaharsu-heading h1 { margin: 4px 0 0; }
.customer-tickets .tickets-breadcrumb { display: inline-flex; align-items: center; min-height: 44px; }
.customer-tickets .tickets-intro { margin-bottom: 20px; }
.customer-tickets .tickets-intro-kicker { margin: 0 0 8px; font-size: 12px; color: #cadbea; }
.customer-tickets .tickets-intro .service-actions { padding-top: 16px; }
.customer-tickets .service-actions a { white-space: normal; text-align: center; }
.tickets-profile-note { display: flex; gap: 12px; padding: 16px; border: 1px solid #cde6f7; border-radius: 20px; background: #f6fbff; }
.tickets-profile-note > div { min-width: 0; }
.customer-tickets .tickets-profile-note h2 { font-size: 16px; margin: 0; }
.tickets-profile-note p { color: #526c86; font-size: 14px; line-height: 1.8; }
.tickets-profile-note .service-icon { width: 36px; height: 36px; flex-shrink: 0; }
.tickets-profile-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 16px; margin-top: 12px; }
.customer-tickets .tickets-text-link { display: inline-flex; align-items: center; min-height: 44px; color: #08376b; text-decoration: underline; text-underline-offset: 4px; }
.customer-tickets .customer-list-heading { scroll-margin-top: 100px; flex-wrap: wrap; }
.tickets-list-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
.tickets-list-card { display: flex; flex-direction: column; min-width: 0; padding: 16px; border: 1px solid #dce5ed; border-radius: 20px; background: #fff; color: #08376b; transition: border-color .18s, transform .18s, box-shadow .18s; }
.tickets-card-top { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px 12px; }
.tickets-status { padding: 4px 12px; border-radius: 20px; background: #e5f4ff; color: #08376b; font-size: 12px; font-weight: 700; }
.tickets-number { color: #526c86; font-size: 12px; }
.customer-tickets .tickets-list-card h3 { margin: 16px 0; font-size: 18px; font-weight: 800; line-height: 1.7; }
.tickets-card-meta { display: flex; flex-direction: column; gap: 4px; margin-top: auto; padding-top: 12px; border-top: 1px solid #e7edf2; color: #526c86; font-size: 14px; }
.tickets-card-meta time { font-size: 12px; margin-top: 4px; }
.tickets-card-link { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 12px; font-size: 14px; font-weight: 700; }
.tickets-card-link svg { width: 18px; height: 18px; }
.tickets-list-card:hover { border-color: #91bfdd; transform: translateY(-3px); box-shadow: 0 15px 35px -25px #08376b70; }
.tickets-empty { grid-column: 1 / -1; margin: 0; }
.customer-tickets .tickets-empty h3 { font-size: 18px; margin: 12px 0 0; }
.tickets-empty .pay-button { margin-top: 12px; }
@media (min-width: 640px) {
.tickets-list-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
.tickets-list-card { padding: 22px; border-radius: 22px; }
}
@media (max-width: 639px) {
.customer-tickets .tickets-intro .service-actions { padding-top: 10px; }
.tickets-profile-note { padding: 14px; }
.tickets-profile-note > .service-icon { display: none; }
}
@media (prefers-reduced-motion: reduce) {
.tickets-list-card { transition: none; }
.tickets-list-card:hover { transform: none; }
}
+31 -14
View File
@@ -1,20 +1,37 @@
{% extends 'base.html' %}{% load static %}
{% extends 'payments/base.html' %}{% load static %}
{% block title %}{{ page_title }}{% endblock %}
{% block extra_head %}{{ block.super }}<link rel="stylesheet" href="{% static 'css/customer-profile.css' %}?v={{ static_asset_version }}">{% endblock %}
{% block content %}
<div class="page-head"><div><p class="eyebrow">تنظیمات حساب</p><h1>پروفایل من</h1><p>اطلاعات شما به کارشناس کمک می‌کند پاسخ دقیق‌تری ارائه کند.</p></div><a class="nav-link" href="{% url 'tickets:list' %}">بازگشت به تیکت‌ها</a></div>
<section class="card form-card">
<div class="badge" dir="ltr">{{ user.mobile }}</div>
<form method="post" class="mt-6" data-disable-on-submit>{% csrf_token %}{{ form.non_field_errors }}
<div class="grid gap-4 sm:grid-cols-2"><div class="field">{{ form.first_name.label_tag }}{{ form.first_name }}{{ form.first_name.errors }}</div><div class="field">{{ form.last_name.label_tag }}{{ form.last_name }}{{ form.last_name.errors }}</div></div>
<div class="field">{{ form.customer_type.label_tag }}{{ form.customer_type }}{{ form.customer_type.errors }}</div>
<section class="location-fields" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}">
<div class="location-heading"><div><h3>موقعیت پیش‌فرض</h3><p>اختیاری است؛ در تیکت‌های جدید به‌صورت خودکار پیشنهاد می‌شود.</p></div><span>اختیاری</span></div>
<div class="grid gap-4 sm:grid-cols-2"><div class="field">{{ form.province.label_tag }}{{ form.province }}<datalist id="profile-province-options" data-province-options></datalist>{{ form.province.errors }}</div><div class="field">{{ form.city.label_tag }}{{ form.city }}<datalist id="profile-city-options" data-city-options></datalist>{{ form.city.errors }}</div></div>
<p class="location-hint" data-location-hint>نام استان را جست‌وجو و انتخاب کنید.</p>
<section class="chaharsu customer-profile" aria-labelledby="profile-title">
<header class="chaharsu-heading">
<a class="profile-breadcrumb" href="{% url 'payments:home' %}">خدمات چهارسو</a>
<div class="profile-heading"><span class="service-icon">{% include 'payments/_icon.html' with name='user' %}</span><div><h1 id="profile-title">پروفایل من</h1><p>اطلاعات شما به کارشناس کمک می‌کند پاسخ دقیق‌تری ارائه کند.</p></div></div>
</header>
<form method="post" class="pay-form customer-profile-form" data-disable-on-submit>{% csrf_token %}
{% if form.errors %}<div class="pay-errors" role="alert"><p>اطلاعات مشخص‌شده را بررسی و دوباره ذخیره کنید.</p>{{ form.non_field_errors }}</div>{% endif %}
<section class="pay-card" aria-labelledby="profile-personal-title">
<div class="profile-card-heading"><h2 id="profile-personal-title">اطلاعات حساب</h2><span class="profile-mobile"><span>شماره همراه</span><bdi>{{ user.mobile }}</bdi></span></div>
<div class="profile-field-grid">
<div class="pay-field">{{ form.first_name.label_tag }}{{ form.first_name }}{{ form.first_name.errors }}</div>
<div class="pay-field">{{ form.last_name.label_tag }}{{ form.last_name }}{{ form.last_name.errors }}</div>
<div class="pay-field">{{ form.customer_type.label_tag }}{{ form.customer_type }}{{ form.customer_type.errors }}</div>
</div>
</section>
<div class="field">{{ form.password.label_tag }}{{ form.password }}{% if form.password.help_text %}<small class="muted">{{ form.password.help_text }}</small>{% endif %}{{ form.password.errors }}</div>
<button type="submit">ذخیره تغییرات</button>
<section class="pay-card" data-location-picker data-locations-url="{% static 'data/iran-locations.json' %}?v={{ static_asset_version }}" aria-labelledby="profile-location-title">
<h2 id="profile-location-title">موقعیت پیش‌فرض</h2>
<p class="profile-guidance">اختیاری است؛ در درخواست‌های جدید به‌صورت خودکار پیشنهاد می‌شود.</p>
<div class="profile-field-grid">
<div class="pay-field">{{ form.province.label_tag }}{{ form.province }}<datalist id="profile-province-options" data-province-options></datalist>{{ form.province.errors }}</div>
<div class="pay-field">{{ form.city.label_tag }}{{ form.city }}<datalist id="profile-city-options" data-city-options></datalist>{{ form.city.errors }}</div>
</div>
<p class="profile-guidance" data-location-hint role="status">نام استان را جست‌وجو و انتخاب کنید.</p>
</section>
<section class="pay-card" aria-labelledby="profile-password-title">
<h2 id="profile-password-title">رمز عبور</h2>
<div class="pay-field">{{ form.password.label_tag }}{{ form.password }}{% if form.password.help_text %}<p class="profile-guidance" id="{{ form.password.auto_id }}_helptext">{{ form.password.help_text }}</p>{% endif %}{{ form.password.errors }}</div>
</section>
<div class="profile-actions"><button class="pay-button" type="submit">ذخیره تغییرات</button><a class="pay-button secondary" href="{% url 'tickets:list' %}">بازگشت به درخواست‌ها</a></div>
</form>
</section>
<script src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script>
{% endblock %}
{% block extra_scripts %}<script defer src="{% static 'js/location-picker.js' %}?v={{ static_asset_version }}"></script>{% endblock %}
+2 -1
View File
@@ -12,6 +12,7 @@
<link rel="stylesheet" href="{% static 'css/tailwind.css' %}?v={{ static_asset_version }}">
<link rel="stylesheet" href="{% static 'css/operations.css' %}?v={{ static_asset_version }}">
<link rel="stylesheet" href="{% static 'css/dispatches.css' %}?v={{ static_asset_version }}">
{% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' or financial_page or request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name == 'list' or request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name == 'new' %}<link rel="stylesheet" href="{% static 'css/customer-dock.css' %}?v={{ static_asset_version }}">{% endif %}
<script defer src="{% static 'vendor/htmx-2.0.3.min.js' %}?v={{ static_asset_version }}"></script><script defer src="{% static 'vendor/alpine-3.14.8.min.js' %}?v={{ static_asset_version }}"></script>
{% block extra_head %}{% endblock %}
</head>
@@ -65,7 +66,7 @@
</nav>{% endblock %}
</div>
{% elif user.is_authenticated %}
{% if financial_page %}<nav class="mobile-dock customer-mobile-dock chaharsu-dock" aria-label="دسترسی سریع مشتری"><a href="{% url 'payments:home' %}" {% if request.resolver_match.url_name == 'home' %}class="active" aria-current="page"{% endif %}><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h7v7H3V3Zm11 0h7v7h-7V3ZM3 14h7v7H3v-7Zm11 0h7v7h-7v-7Z"/></svg>خدمات</a><a href="{% url 'tickets:list' %}"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v12H9l-5 4V5Zm4 4h8m-8 4h5"/></svg>درخواست‌ها</a><a href="{% url 'accounts:profile' %}"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0ZM4 21v-2a8 8 0 0 1 16 0v2"/></svg>پروفایل</a></nav>{% else %} <nav class="mobile-dock customer-mobile-dock" aria-label="دسترسی سریع مشتری"><a class="{% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}active{% endif %}" href="{% url 'tickets:list' %}" {% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}aria-current="page"{% endif %}><span class="dock-icon">◎</span>درخواست‌ها</a><a class="{% if request.resolver_match.url_name == 'new' %}active{% endif %}" href="{% url 'tickets:new' %}" {% if request.resolver_match.url_name == 'new' %}aria-current="page"{% endif %}><span class="dock-icon">+</span>درخواست جدید</a><a class="{% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}active{% endif %}" href="{% url 'accounts:profile' %}" {% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}aria-current="page"{% endif %}><span class="dock-icon">○</span>پروفایل</a><form method="post" action="{% url 'accounts:logout' %}">{% csrf_token %}<button type="submit"><span class="dock-icon">←</span>خروج</button></form></nav>{% endif %}
{% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' or financial_page or request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name == 'list' or request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name == 'new' %}{% include 'tickets/_customer_dock.html' %}{% else %} <nav class="mobile-dock customer-mobile-dock" aria-label="دسترسی سریع مشتری"><a class="{% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}active{% endif %}" href="{% url 'tickets:list' %}" {% if request.resolver_match.namespace == 'tickets' and request.resolver_match.url_name != 'new' %}aria-current="page"{% endif %}><span class="dock-icon">◎</span>درخواست‌ها</a><a class="{% if request.resolver_match.url_name == 'new' %}active{% endif %}" href="{% url 'tickets:new' %}" {% if request.resolver_match.url_name == 'new' %}aria-current="page"{% endif %}><span class="dock-icon">+</span>درخواست جدید</a><a class="{% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}active{% endif %}" href="{% url 'accounts:profile' %}" {% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}aria-current="page"{% endif %}><span class="dock-icon">○</span>پروفایل</a><form method="post" action="{% url 'accounts:logout' %}">{% csrf_token %}<button type="submit"><span class="dock-icon">←</span>خروج</button></form></nav>{% endif %}
{% endif %}
{% block pwa_install_prompt %}{% endblock %}
<script src="{% static 'js/voice-recorder.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/ui.js' %}?v={{ static_asset_version }}"></script><script src="{% static 'js/chat-coach.js' %}?v={{ static_asset_version }}"></script>
+14
View File
@@ -0,0 +1,14 @@
<nav class="mobile-dock customer-mobile-dock chaharsu-dock" aria-label="دسترسی سریع مشتری">
<a href="{% url 'payments:home' %}" {% if request.resolver_match.namespace == 'payments' and request.resolver_match.url_name == 'home' %}class="active" aria-current="page"{% endif %}>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3h7v7H3V3Zm11 0h7v7h-7V3ZM3 14h7v7H3v-7Zm11 0h7v7h-7v-7Z"/></svg>
خدمات
</a>
<a href="{% url 'tickets:list' %}" {% if request.resolver_match.namespace == 'tickets' %}class="active" {% if request.resolver_match.url_name == 'list' %}aria-current="page"{% else %}aria-current="true"{% endif %}{% endif %}>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16v12H9l-5 4V5Zm4 4h8m-8 4h5"/></svg>
درخواست‌ها
</a>
<a href="{% url 'accounts:profile' %}" {% if request.resolver_match.namespace == 'accounts' and request.resolver_match.url_name == 'profile' %}class="active" aria-current="page"{% endif %}>
<svg aria-hidden="true" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"><path d="M16 7a4 4 0 1 1-8 0 4 4 0 0 1 8 0ZM4 21v-2a8 8 0 0 1 16 0v2"/></svg>
پروفایل
</a>
</nav>
+27 -17
View File
@@ -1,29 +1,39 @@
{% extends 'base.html' %}{% load jalali %}
{% extends 'payments/base.html' %}{% load static jalali %}
{% block title %}{{ page_title }}{% endblock %}
{% block extra_head %}{{ block.super }}<link rel="stylesheet" href="{% static 'css/customer-tickets.css' %}?v={{ static_asset_version }}">{% endblock %}
{% block content %}
{% if site_branding.customer_tickets_hero_enabled %}<section class="hero-panel">
<p class="page-kicker">{{ site_branding.customer_tickets_hero_eyebrow }}</p>
<h1>{{ site_branding.customer_tickets_hero_heading|linebreaksbr }}</h1>
<p>{{ site_branding.customer_tickets_hero_body|linebreaksbr }}</p>
<div class="hero-actions">
<a class="button" href="{% url 'tickets:new' %}"><span>+</span> ثبت درخواست جدید</a>
<a class="button hero-history-button" href="#recent-requests"><span aria-hidden="true">↓</span> درخواست‌های قبلی</a>
<section class="chaharsu customer-tickets" aria-labelledby="customer-tickets-title">
<header class="chaharsu-heading"><a class="tickets-breadcrumb" href="{% url 'payments:home' %}">خدمات چهارسو</a><h1 id="customer-tickets-title">درخواست‌های من</h1><p>درخواست‌ها و پاسخ کارشناسان را از اینجا پیگیری کنید.</p></header>
{% if site_branding.customer_tickets_hero_enabled %}<section class="expert-service tickets-intro">
{% if site_branding.customer_tickets_hero_eyebrow %}<p class="tickets-intro-kicker">{{ site_branding.customer_tickets_hero_eyebrow }}</p>{% endif %}
<div class="expert-service-head">
<span class="service-icon">{% include 'payments/_icon.html' with name='chat' %}</span>
<h2>{{ site_branding.customer_tickets_hero_heading|linebreaksbr }}</h2>
<p>{{ site_branding.customer_tickets_hero_body|linebreaksbr }}</p>
</div>
<div class="service-actions">
<a class="pay-button light" href="{% url 'tickets:new' %}">{% include 'payments/_icon.html' with name='plus' %}ثبت درخواست جدید</a>
<a class="pay-button outline-light" href="#recent-requests">{% include 'payments/_icon.html' with name='history' %}درخواست‌های قبلی</a>
</div>
</section>{% endif %}
{% if missing_profile_fields %}<aside class="profile-completion-notice" role="note"><span class="profile-completion-icon" aria-hidden="true">◎</span><div><h2>تکمیل پروفایل، برای پیگیری دقیق‌تر</h2><p>مشتری گرامی، اطلاعات <b>{{ missing_profile_fields|join:'، ' }}</b> در پروفایل شما تکمیل نشده است. ثبت این اطلاعات اختیاری است؛ بااین‌حال توصیه می‌کنیم پیش از ثبت درخواست، آن‌ها را وارد کنید تا کارشناسان فروش بتوانند دقیق‌تر و سریع‌تر راهنمایی‌تان کنند.</p><div class="profile-completion-actions"><a class="button" href="{% url 'accounts:profile' %}">تکمیل پروفایل</a><a href="{% url 'tickets:new' %}">فعلاً ثبت درخواست را ادامه می‌دهم</a></div></div></aside>{% endif %}
<div class="page-head" id="recent-requests">
<div><p class="page-kicker">پیگیری‌ها</p><h2>درخواست‌های اخیر شما</h2></div>
<span class="muted">{{ tickets|length }} مورد</span>
{% if missing_profile_fields %}<aside class="tickets-profile-note" role="note"><span class="service-icon">{% include 'payments/_icon.html' with name='user' %}</span><div><h2>تکمیل پروفایل، برای پیگیری دقیق‌تر</h2><p>اطلاعات <b>{{ missing_profile_fields|join:'، ' }}</b> در پروفایل شما تکمیل نشده است. تکمیل آن‌ها اختیاری است و به راهنمایی دقیق‌تر کارشناسان کمک می‌کند.</p><div class="tickets-profile-actions"><a class="pay-button secondary" href="{% url 'accounts:profile' %}">تکمیل پروفایل</a><a class="tickets-text-link" href="{% url 'tickets:new' %}">فعلاً ثبت درخواست را ادامه می‌دهم</a></div></div></aside>{% endif %}
<div class="customer-list-heading" id="recent-requests">
<h2>درخواست‌های اخیر شما</h2>
<span>{{ tickets|length }} مورد</span>
</div>
<div class="ticket-grid">
<div class="tickets-list-grid">
{% for ticket in tickets %}
<a class="ticket-card" href="{% url 'tickets:detail' ticket.public_id %}">
<div><div class="ticket-card-top"><span class="ticket-number" dir="ltr">{{ ticket.ticket_number }}</span><span class="badge">{{ ticket.get_status_display }}</span></div><h2>{{ ticket.subject }}</h2></div>
<div class="ticket-meta"><div><small>{{ ticket.category }}</small><div class="expert-line">{{ ticket.assigned_expert_name }}</div></div><small>{{ ticket.last_activity_at|jalali_datetime }}</small></div>
<a class="tickets-list-card" href="{% url 'tickets:detail' ticket.public_id %}">
<div class="tickets-card-top"><span class="tickets-status">{{ ticket.get_status_display }}</span><bdi class="tickets-number">{{ ticket.ticket_number }}</bdi></div>
<h3>{{ ticket.subject }}</h3>
<div class="tickets-card-meta"><span>{{ ticket.category }}</span><span>کارشناس: {{ ticket.assigned_expert_name }}</span><time datetime="{{ ticket.last_activity_at|date:'c' }}">{{ ticket.last_activity_at|jalali_datetime }}</time></div>
<span class="tickets-card-link">مشاهده و پیگیری{% include 'payments/_icon.html' with name='arrow' %}</span>
</a>
{% empty %}
<div class="empty"><div class="empty-icon">✦</div><h2>هنوز درخواستی ثبت نشده</h2><p class="muted">اولین گفت‌وگوی خود را با تیم فروش آغاز کنید.</p><a class="button" href="{% url 'tickets:new' %}">ثبت اولین درخواست</a></div>
<div class="pay-card customer-empty tickets-empty">{% include 'payments/_icon.html' with name='chat' %}<h3>هنوز درخواستی ثبت نشده</h3><p>اولین گفت‌وگوی خود را با تیم فروش آغاز کنید.</p><a class="pay-button" href="{% url 'tickets:new' %}">{% include 'payments/_icon.html' with name='plus' %}ثبت اولین درخواست</a></div>
{% endfor %}
</div>
</section>
{% endblock %}
{% block extra_scripts %}{% endblock %}
{% block pwa_install_prompt %}<aside class="pwa-install" data-pwa-install hidden><img class="pwa-install-icon" src="{{ pwa_icon_url }}?v={{ static_asset_version }}-{{ branding_version }}" alt="" width="64" height="64"><div class="pwa-install-copy"><b>{{ site_branding.pwa_install_label }}</b><small data-pwa-help role="status">{{ site_branding.pwa_install_help }}</small></div><div class="pwa-install-actions"><button type="button" data-pwa-dismiss>بعداً</button><button type="button" class="primary" data-pwa-accept>نصب</button></div></aside>{% endblock %}
+38
View File
@@ -0,0 +1,38 @@
from playwright.sync_api import expect
from .base import BasePage
class CustomerProfilePage(BasePage):
def open(self):
self.goto('/profile/')
expect(self.page.get_by_role('heading', name='پروفایل من', exact=True)).to_be_visible()
def edit_details(self):
self.page.get_by_label('نام:', exact=True).fill('مریم')
self.page.get_by_label('نام خانوادگی:', exact=True).fill('رضایی')
self.page.get_by_label('نوع مشتری:', exact=True).select_option('PERSON')
expect(self.page.locator('#profile-province-options option')).not_to_have_count(0)
self.page.get_by_label('استان:', exact=True).fill('تهران')
expect(self.page.get_by_label('شهر:', exact=True)).to_be_enabled()
self.page.get_by_label('شهر:', exact=True).fill('تهران')
def assert_validation_and_save(self):
self.page.get_by_label('رمز عبور جدید:', exact=True).fill('short')
self.page.get_by_role('button', name='ذخیره تغییرات', exact=True).click()
expect(self.page.get_by_label('رمز عبور جدید:', exact=True)).to_be_focused()
assert self.page.get_by_label('رمز عبور جدید:', exact=True).evaluate('(input) => input.validity.tooShort')
self.page.get_by_label('رمز عبور جدید:', exact=True).fill('')
self.page.get_by_label('شهر:', exact=True).fill('رشت')
self.page.get_by_label('شهر:', exact=True).press('Tab')
self.page.get_by_role('button', name='ذخیره تغییرات', exact=True).click()
expect(self.page.get_by_role('alert')).to_contain_text('اطلاعات مشخص‌شده را بررسی')
expect(self.page.get_by_label('نام:', exact=True)).to_have_value('مریم')
expect(self.page.get_by_label('شهر:', exact=True)).to_have_attribute('aria-invalid', 'true')
expect(self.page.locator('#profile-city-options option[value="تهران"]')).to_have_count(1)
self.page.get_by_label('شهر:', exact=True).fill('تهران')
self.page.get_by_label('شهر:', exact=True).press('Tab')
self.page.get_by_role('button', name='ذخیره تغییرات', exact=True).click()
expect(self.page.locator('.flash')).to_contain_text('پروفایل شما ذخیره شد.')
expect(self.page.get_by_label('نام:', exact=True)).to_have_value('مریم')
expect(self.page.get_by_label('شهر:', exact=True)).to_have_value('تهران')
+39
View File
@@ -0,0 +1,39 @@
from django.urls import reverse
from playwright.sync_api import expect
from .base import BasePage
class CustomerTicketsPage(BasePage):
def assert_dock(self, active):
dock = self.page.get_by_role('navigation', name='دسترسی سریع مشتری')
expect(dock.get_by_role('link')).to_have_count(3)
for name in ('خدمات', 'درخواست‌ها', 'پروفایل'):
expect(dock.get_by_role('link', name=name, exact=True)).to_be_visible()
expect(dock.locator('[aria-current]')).to_have_text(active)
for link in dock.get_by_role('link').all():
box = link.bounding_box()
assert box['width'] >= 44 and box['height'] >= 44
def follow_dock(self, name, destination):
self.page.get_by_role('navigation', name='دسترسی سریع مشتری').get_by_role(
'link', name=name, exact=True
).click()
expect(self.page).to_have_url(self.url(reverse(destination)))
def assert_history(self, subject=None):
expect(self.page.get_by_role('heading', name='درخواست‌های من', exact=True)).to_be_visible()
if subject:
card = self.page.locator('.tickets-list-card').filter(has_text=subject)
expect(card.get_by_role('heading', name=subject, exact=True)).to_be_visible()
expect(card).to_contain_text('مشاهده و پیگیری')
else:
expect(self.page.get_by_role('heading', name='هنوز درخواستی ثبت نشده')).to_be_visible()
expect(self.page.get_by_role('link', name='ثبت اولین درخواست')).to_be_visible()
assert self.page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
def start_request(self):
self.page.get_by_role('link', name='ثبت درخواست جدید', exact=True).click()
expect(self.page).to_have_url(self.url(reverse('tickets:new')))
expect(self.page.locator('.ticket-wizard-page')).to_be_visible()
+38
View File
@@ -0,0 +1,38 @@
import pytest
from apps.accounts.models import User
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.customer_profile import CustomerProfilePage
from tests.e2e.pages.customer_tickets import CustomerTicketsPage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('width', [320, 393, 1440])
def test_profile_design_navigation_validation_and_save(live_server, browser_page, width):
password = 'Profile-design-2026!'
customer = User.objects.create_user(
username='profile-design', mobile='09120008892', password=password, account_type='CUSTOMER'
)
with browser_page() as page:
page.set_viewport_size({'width': width, 'height': 856})
AuthenticationPage(page, live_server.url).login_customer(customer, password)
navigation = CustomerTicketsPage(page, live_server.url)
profile = CustomerProfilePage(page, live_server.url)
if width < 640:
navigation.follow_dock('پروفایل', 'accounts:profile')
navigation.assert_dock('پروفایل')
else:
profile.open()
profile.edit_details()
profile.assert_validation_and_save()
assert page.evaluate('document.documentElement.scrollWidth <= innerWidth + 1')
if width < 640:
navigation.assert_dock('پروفایل')
navigation.follow_dock('درخواست‌ها', 'tickets:list')
navigation.assert_dock('درخواست‌ها')
navigation.follow_dock('خدمات', 'payments:home')
navigation.assert_dock('خدمات')
customer.customer_profile.refresh_from_db()
assert customer.customer_profile.first_name == 'مریم'
assert customer.customer_profile.city == 'تهران'
+42
View File
@@ -0,0 +1,42 @@
import pytest
from apps.accounts.models import User
from apps.tickets.models import TicketCategory
from apps.tickets.services import create_ticket
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.customer_tickets import CustomerTicketsPage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
@pytest.mark.parametrize('width', [320, 393, 1440])
@pytest.mark.parametrize('has_ticket', [False, True], ids=['empty', 'history'])
def test_customer_history_and_composer_share_chaharsu_navigation(live_server, browser_page, width, has_ticket):
password = 'Design-test-2026!'
customer = User.objects.create_user(
username='customer-design', mobile='09120008891', password=password, account_type='CUSTOMER'
)
subject = 'پیگیری سفارش و هماهنگی ارسال کالا با کارشناسان فروش برای تحویل در محل مشتری'
if has_ticket:
category = TicketCategory.objects.create(title='هماهنگی ارسال', slug='design-shipping')
create_ticket(customer=customer, category=category, subject=subject, body='شرح درخواست')
with browser_page() as page:
page.set_viewport_size({'width': width, 'height': 856})
AuthenticationPage(page, live_server.url).login_customer(customer, password)
history = CustomerTicketsPage(page, live_server.url)
history.goto('/chaharsu/')
if width < 640:
history.assert_dock('خدمات')
history.follow_dock('درخواست‌ها', 'tickets:list')
else:
history.goto('/tickets/')
history.assert_history(subject if has_ticket else None)
if width < 640:
history.assert_dock('درخواست‌ها')
history.start_request()
if width < 640:
history.assert_dock('درخواست‌ها')
history.follow_dock('درخواست‌ها', 'tickets:list')
history.assert_history(subject if has_ticket else None)
history.follow_dock('خدمات', 'payments:home')
history.assert_dock('خدمات')