-num lines-num-old"> 341
+msgid "Pickup & Logistics"
342
+msgstr "การรับของและโลจิสติกส์"
343
+
344
+#: recycle_core/models.py
345
+msgid "Material Sorting"
346
+msgstr "การคัดแยกวัสดุ"
347
+
348
+#: recycle_core/models.py
349
+msgid "Weighing & Ticketing"
350
+msgstr "การชั่งน้ำหนักและออกบัตรชั่ง"
351
+
352
+#: recycle_core/models.py
353
+msgid "Invoicing & Payouts"
354
+msgstr "การวางบิลและการจ่ายเงิน"
355
+
356
+#: recycle_core/models.py
357
+msgid "Reporting & Analytics"
358
+msgstr "การรายงานและการวิเคราะห์"
359
+
360
+#: recycle_core/models.py
361
+msgid "Marketplace & Bidding"
362
+msgstr "มาร์เก็ตเพลสและการประมูล"
363
+
364
+#: recycle_core/models.py
365
+msgid "Compliance & Audits"
366
+msgstr "การปฏิบัติตามและการตรวจสอบ"
367
+
368
+#: recycle_core/models.py
369
+msgid "Consulting & Training"
370
+msgstr "ที่ปรึกษาและการฝึกอบรม"

+ 18 - 0
orgs/apps.py

@@ -14,4 +14,22 @@ class OrgsConfig(AppConfig):
14 14
         except Exception:
15 15
             # Avoid startup hard-fail if migrations/apps not ready; re-raise only in debug if needed
16 16
             raise
17
+
18
+        # Monkey-patch User.__str__ to show "<username>, <fullname>"
19
+        # This affects labels in forms and general stringification.
20
+        try:
21
+            from django.contrib.auth import get_user_model
22
+
23
+            User = get_user_model()
24
+
25
+            def _user_str(self):  # type: ignore[override]
26
+                full = self.get_full_name().strip()
27
+                return f"{self.username}, {full}" if full else f"{self.username}"
28
+
29
+            # Assign only if not already customized
30
+            if getattr(User.__str__, "__name__", None) != "_user_str":  # type: ignore[attr-defined]
31
+                User.__str__ = _user_str  # type: ignore[assignment]
32
+        except Exception:
33
+            # Do not block app startup if something goes wrong
34
+            pass
17 35
         return super().ready()

+ 2 - 1
orgs/views_admin.py

@@ -165,7 +165,8 @@ def permissions_edit_group(request, pk: int):
165 165
         final_ids = [pid for pid in sel_ids if pid in allowed_ids]
166 166
         group.permissions.set(Permission.objects.filter(id__in=final_ids))
167 167
         messages.success(request, f"Permissions updated for group '{group.name}'.")
168
-        return redirect("orgs_admin:permissions_overview")
168
+        # Redirect back to the current URL (stay on the edit page)
169
+        return redirect(request.get_full_path())
169 170
 
170 171
     current_ids = set(group.permissions.values_list("id", flat=True))
171 172
 

+ 22 - 8
public_frontend/templates/public_frontend/base.html

@@ -1,6 +1,7 @@
1
-{% load tailwind_tags static %}
1
+{% load tailwind_tags static i18n %}
2 2
 <!DOCTYPE html>
3
-<html lang="en">
3
+{% get_current_language as LANGUAGE_CODE %}
4
+<html lang="{{ LANGUAGE_CODE }}">
4 5
 <head>
5 6
   <meta charset="utf-8" />
6 7
   <meta name="viewport" content="width=device-width, initial-scale=1" />
@@ -13,12 +14,25 @@
13 14
     <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 flex items-center justify-between h-16">
14 15
       <a href="/" class="text-lg font-semibold">{% if current_site %}{{ current_site.name }}{% else %}Ecoloop{% endif %}</a>
15 16
       <nav class="hidden md:flex items-center gap-6">
16
-        <a href="/#materials" class="hover:text-black/70">Materials</a>
17
-        <a href="/#listings" class="hover:text-black/70">Listings</a>
18
-        <a href="/#pickup-request" class="hover:text-black/70">Request Pickup</a>
19
-        <a href="/#blog" class="hover:text-black/70">Blog</a>
20
-        <a href="/#contact" class="hover:text-black/70">Contact</a>
17
+        <a href="/#materials" class="hover:text-black/70">{% trans "Materials" %}</a>
18
+        <a href="/#listings" class="hover:text-black/70">{% trans "Listings" %}</a>
19
+        <a href="/#pickup-request" class="hover:text-black/70">{% trans "Request Pickup" %}</a>
20
+        <a href="/#blog" class="hover:text-black/70">{% trans "Blog" %}</a>
21
+        <a href="/#contact" class="hover:text-black/70">{% trans "Contact" %}</a>
21 22
       </nav>
23
+      <form method="post" action="{% url 'set_language' %}" class="hidden md:block ml-4">
24
+        {% csrf_token %}
25
+        <input type="hidden" name="next" value="{{ request.get_full_path }}" />
26
+        {% get_available_languages as langs %}
27
+        {% get_language_info_list for langs as languages %}
28
+        <label for="lang" class="sr-only">Language</label>
29
+        <select id="lang" name="language" class="text-sm border border-gray-300 rounded px-2 py-1 bg-white"
30
+                onchange="this.form.submit()">
31
+          {% for lang in languages %}
32
+            <option value="{{ lang.code }}" {% if lang.code == LANGUAGE_CODE %}selected{% endif %}>{{ lang.name_local }}</option>
33
+          {% endfor %}
34
+        </select>
35
+      </form>
22 36
     </div>
23 37
   </header>
24 38
   <main class="mx-auto max-w-7xl p-4 sm:p-6 lg:p-8 min-h-[80vh]">
@@ -36,7 +50,7 @@
36 50
   <footer class="bg-white border-t">
37 51
     <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between text-sm text-gray-600">
38 52
       <span>&copy; {% now "Y" %} {% if current_site %}{{ current_site.name }}{% else %}Ecoloop{% endif %}</span>
39
-      <a href="/webadmin/" class="hover:underline">Staff Login</a>
53
+      <a href="/webadmin/" class="hover:underline">{% trans "Staff Login" %}</a>
40 54
     </div>
41 55
   </footer>
42 56
 </body>

+ 1 - 0
public_frontend/templates/public_frontend/blog_detail.html

@@ -1,4 +1,5 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
+{% load i18n %}
2 3
 {% block title %}{{ post.title }}{% endblock %}
3 4
 {% block content %}
4 5
 <article class="blog-article max-w-none bg-white rounded shadow p-6">

+ 4 - 3
public_frontend/templates/public_frontend/blog_list.html

@@ -1,5 +1,6 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Blog{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Blog" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="grid gap-4">
5 6
   {% for p in posts %}
@@ -10,7 +11,7 @@
10 11
             <img src="{{ p.feature_image.url }}" alt="{{ p.title }}" loading="lazy">
11 12
           </div>
12 13
         {% else %}
13
-          <div class="thumb placeholder">No image</div>
14
+          <div class="thumb placeholder">{% trans "No image" %}</div>
14 15
         {% endif %}
15 16
         <div class="min-w-0">
16 17
           <div class="text-sm text-gray-500">{{ p.published_at|date:'M j, Y' }}</div>
@@ -20,7 +21,7 @@
20 21
       </div>
21 22
     </a>
22 23
   {% empty %}
23
-    <div class="text-gray-500">No posts yet.</div>
24
+    <div class="text-gray-500">{% trans "No posts yet." %}</div>
24 25
   {% endfor %}
25 26
   </div>
26 27
 <div class="mt-4">{% include 'admin_frontend/_pagination.html' %}</div>

+ 9 - 8
public_frontend/templates/public_frontend/contact.html

@@ -1,35 +1,36 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Contact{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Contact" %}{% endblock %}
3 4
 {% block content %}
4
-<h1 class="text-xl font-semibold mb-4">Contact Us{% if org %} — {{ org.name }}{% endif %}</h1>
5
+<h1 class="text-xl font-semibold mb-4">{% trans "Contact Us" %}{% if org %} — {{ org.name }}{% endif %}</h1>
5 6
 <form method="post" class="bg-white rounded shadow p-4 grid gap-4">
6 7
   {% csrf_token %}
7 8
   <div class="grid md:grid-cols-2 gap-4">
8 9
     <div>
9
-      <label class="block text-sm font-medium mb-1">Name</label>
10
+      <label class="block text-sm font-medium mb-1">{% trans "Name" %}</label>
10 11
       <input type="text" name="name" value="{{ form.name.value|default:'' }}" required class="w-full border rounded px-3 py-2">
11 12
     </div>
12 13
     <div>
13
-      <label class="block text-sm font-medium mb-1">Email</label>
14
+      <label class="block text-sm font-medium mb-1">{% trans "Email" %}</label>
14 15
       <input type="email" name="email" value="{{ form.email.value|default:'' }}" required class="w-full border rounded px-3 py-2">
15 16
     </div>
16 17
   </div>
17 18
   <div class="grid md:grid-cols-2 gap-4">
18 19
     <div>
19
-      <label class="block text-sm font-medium mb-1">Phone</label>
20
+      <label class="block text-sm font-medium mb-1">{% trans "Phone" %}</label>
20 21
       <input type="text" name="phone" value="{{ form.phone.value|default:'' }}" class="w-full border rounded px-3 py-2">
21 22
     </div>
22 23
     <div>
23
-      <label class="block text-sm font-medium mb-1">Subject</label>
24
+      <label class="block text-sm font-medium mb-1">{% trans "Subject" %}</label>
24 25
       <input type="text" name="subject" value="{{ form.subject.value|default:'' }}" class="w-full border rounded px-3 py-2">
25 26
     </div>
26 27
   </div>
27 28
   <div>
28
-    <label class="block text-sm font-medium mb-1">Message</label>
29
+    <label class="block text-sm font-medium mb-1">{% trans "Message" %}</label>
29 30
     <textarea name="message" rows="5" class="w-full border rounded px-3 py-2">{{ form.message.value|default:'' }}</textarea>
30 31
   </div>
31 32
   <div>
32
-    <button class="btn-primary" type="submit">Send</button>
33
+    <button class="btn-primary" type="submit">{% trans "Send" %}</button>
33 34
   </div>
34 35
 </form>
35 36
 <style>.btn-primary{background:#1d4ed8;color:white;padding:.5rem .75rem;border-radius:.375rem}</style>

+ 24 - 24
public_frontend/templates/public_frontend/home.html

@@ -1,16 +1,16 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% load static %}
3
-{% block title %}Home{% endblock %}
2
+{% load static i18n %}
3
+{% block title %}{% trans "Home" %}{% endblock %}
4 4
 
5 5
 {% block content %}
6 6
 <div class="grid gap-12 md:gap-16">
7 7
   {# Hero Section #}
8 8
   <section id="hero" class="bg-white rounded-lg shadow-md p-6 md:p-8 text-center">
9
-    <h1 class="text-3xl md:text-4xl font-bold mb-2">Streamline Your Factory's Recycling</h1>
10
-    <p class="text-gray-600 max-w-2xl mx-auto mb-6">We partner with businesses like yours to manage scrap materials efficiently. Browse listings, request a pickup, and turn your waste into a resource.</p>
9
+    <h1 class="text-3xl md:text-4xl font-bold mb-2">{% trans "Streamline Your Factory's Recycling" %}</h1>
10
+    <p class="text-gray-600 max-w-2xl mx-auto mb-6">{% trans "We partner with businesses like yours to manage scrap materials efficiently. Browse listings, request a pickup, and turn your waste into a resource." %}</p>
11 11
     <div class="flex gap-3 justify-center">
12
-      <a href="{% url 'public_frontend:pickup_request' %}" class="btn-primary">Request a Pickup</a>
13
-      <a href="#listings" class="btn-outline">Browse Listings</a>
12
+      <a href="{% url 'public_frontend:pickup_request' %}" class="btn-primary">{% trans "Request a Pickup" %}</a>
13
+      <a href="#listings" class="btn-outline">{% trans "Browse Listings" %}</a>
14 14
     </div>
15 15
   </section>
16 16
 
@@ -18,14 +18,14 @@
18 18
   {% if services %}
19 19
   <section id="services">
20 20
     <div class="flex items-center justify-between mb-3">
21
-      <h2 class="text-2xl font-semibold">Our Services</h2>
21
+      <h2 class="text-2xl font-semibold">{% trans "Our Services" %}</h2>
22 22
     </div>
23 23
     <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
24 24
       {% for s in services %}
25 25
         <a href="{% url 'public_frontend:service_detail' s.pk %}" class="block p-4 bg-white rounded-lg shadow-md hover:bg-gray-50 transition flex items-start gap-4">
26 26
           <img src="{% static 'service_icons/' %}{{ s.image_name }}" alt="{{ s.title }} icon" class="w-12 h-12 object-contain"/>
27 27
           <div>
28
-            <div class="font-medium">{{ s.title }}</div>
28
+            <div class="font-medium">{{ s.get_title_display }}</div>
29 29
             <div class="text-sm text-gray-600">{{ s.description }}</div>
30 30
           </div>
31 31
         </a>
@@ -37,8 +37,8 @@
37 37
   {# Materials Section #}
38 38
   <section id="materials">
39 39
     <div class="flex items-center justify-between mb-3">
40
-      <h2 class="text-2xl font-semibold">Materials We Accept</h2>
41
-      <a href="{% url 'public_frontend:materials_list' %}" class="text-blue-700 hover:underline">View all</a>
40
+      <h2 class="text-2xl font-semibold">{% trans "Materials We Accept" %}</h2>
41
+      <a href="{% url 'public_frontend:materials_list' %}" class="text-blue-700 hover:underline">{% trans "View all" %}</a>
42 42
     </div>
43 43
     <div class="bg-white rounded-lg shadow-md p-4">
44 44
         <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
@@ -48,7 +48,7 @@
48 48
                     <p class="text-sm text-gray-500">{{ m.category.name }}</p>
49 49
                 </div>
50 50
             {% empty %}
51
-                <p class="text-gray-500 col-span-full">No materials published.</p>
51
+                <p class="text-gray-500 col-span-full">{% trans "No materials published." %}</p>
52 52
             {% endfor %}
53 53
         </div>
54 54
     </div>
@@ -57,18 +57,18 @@
57 57
   {# Open Listings Section #}
58 58
   <section id="listings">
59 59
     <div class="flex items-center justify-between mb-3">
60
-      <h2 class="text-2xl font-semibold">Open Listings</h2>
61
-      <a href="{% url 'public_frontend:listings_list' %}" class="text-blue-700 hover:underline">Browse all</a>
60
+      <h2 class="text-2xl font-semibold">{% trans "Open Listings" %}</h2>
61
+      <a href="{% url 'public_frontend:listings_list' %}" class="text-blue-700 hover:underline">{% trans "Browse all" %}</a>
62 62
     </div>
63 63
     <div class="grid md:grid-cols-3 gap-4">
64 64
       {% for l in listings %}
65 65
         <a href="{% url 'public_frontend:listing_detail' l.id %}" class="block p-4 border rounded bg-white hover:bg-gray-50 transition">
66
-          <div class="text-sm text-gray-500">Ends {{ l.ends_at|default:'—' }}</div>
66
+          <div class="text-sm text-gray-500">{% trans "Ends" %} {{ l.ends_at|default:'—' }}</div>
67 67
           <div class="font-medium">{{ l.title }}</div>
68 68
           <div class="text-sm text-gray-600 line-clamp-2">{{ l.description|truncatechars:120 }}</div>
69 69
         </a>
70 70
       {% empty %}
71
-        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">No public listings at the moment.</div>
71
+        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">{% trans "No public listings at the moment." %}</div>
72 72
       {% endfor %}
73 73
     </div>
74 74
   </section>
@@ -78,8 +78,8 @@
78 78
   {# Blog Section #}
79 79
   <section id="blog">
80 80
     <div class="flex items-center justify-between mb-3">
81
-      <h2 class="text-2xl font-semibold">Industry Insights</h2>
82
-      <a href="{% url 'public_frontend:blog_list' %}" class="text-blue-700 hover:underline">View all</a>
81
+      <h2 class="text-2xl font-semibold">{% trans "Industry Insights" %}</h2>
82
+      <a href="{% url 'public_frontend:blog_list' %}" class="text-blue-700 hover:underline">{% trans "View all" %}</a>
83 83
     </div>
84 84
     <div class="grid md:grid-cols-3 gap-4">
85 85
       {% for p in posts %}
@@ -89,36 +89,36 @@
89 89
           <div class="text-sm text-gray-600 line-clamp-2">{{ p.excerpt|default:p.content|striptags|truncatechars:120 }}</div>
90 90
         </a>
91 91
       {% empty %}
92
-        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">No posts yet.</div>
92
+        <div class="text-gray-500 p-4 bg-white rounded-lg shadow-md col-span-full">{% trans "No posts yet." %}</div>
93 93
       {% endfor %}
94 94
     </div>
95 95
   </section>
96 96
 
97 97
   {# Contact Section #}
98 98
   <section id="contact">
99
-    <h2 class="text-2xl font-semibold mb-3">Contact Us</h2>
99
+    <h2 class="text-2xl font-semibold mb-3">{% trans "Contact Us" %}</h2>
100 100
     <form method="post" action="{% url 'public_frontend:contact' %}" class="bg-white rounded-lg shadow-md p-4 grid gap-4">
101 101
       {% csrf_token %}
102 102
       <div class="grid md:grid-cols-2 gap-4">
103 103
         <div>
104
-          <label class="block text-sm font-medium mb-1">Name</label>
104
+          <label class="block text-sm font-medium mb-1">{% trans "Name" %}</label>
105 105
           <input type="text" name="name" value="{{ contact_form.name.value|default:'' }}" required class="w-full border rounded px-3 py-2">
106 106
         </div>
107 107
         <div>
108
-          <label class="block text-sm font-medium mb-1">Email</label>
108
+          <label class="block text-sm font-medium mb-1">{% trans "Email" %}</label>
109 109
           <input type="email" name="email" value="{{ contact_form.email.value|default:'' }}" required class="w-full border rounded px-3 py-2">
110 110
         </div>
111 111
       </div>
112 112
       <div>
113
-        <label class="block text-sm font-medium mb-1">Subject</label>
113
+        <label class="block text-sm font-medium mb-1">{% trans "Subject" %}</label>
114 114
         <input type="text" name="subject" value="{{ contact_form.subject.value|default:'' }}" class="w-full border rounded px-3 py-2">
115 115
       </div>
116 116
       <div>
117
-        <label class="block text-sm font-medium mb-1">Message</label>
117
+        <label class="block text-sm font-medium mb-1">{% trans "Message" %}</label>
118 118
         <textarea name="message" rows="5" class="w-full border rounded px-3 py-2">{{ contact_form.message.value|default:'' }}</textarea>
119 119
       </div>
120 120
       <div>
121
-        <button class="btn-primary" type="submit">Send Message</button>
121
+        <button class="btn-primary" type="submit">{% trans "Send Message" %}</button>
122 122
       </div>
123 123
     </form>
124 124
   </section>

+ 5 - 4
public_frontend/templates/public_frontend/listing_detail.html

@@ -1,16 +1,17 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Listing {{ listing.id }}{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Listing" %} {{ listing.id }}{% endblock %}
3 4
 {% block content %}
4 5
 <article class="bg-white rounded shadow p-6">
5 6
   <h1 class="text-2xl font-semibold mb-1">{{ listing.title }}</h1>
6
-  <div class="text-sm text-gray-500 mb-4">Status: {{ listing.status }}{% if listing.ends_at %} • Ends {{ listing.ends_at }}{% endif %}</div>
7
+  <div class="text-sm text-gray-500 mb-4">{% trans "Status" %}: {{ listing.status }}{% if listing.ends_at %} • {% trans "Ends" %} {{ listing.ends_at }}{% endif %}</div>
7 8
   <p class="prose max-w-none">{{ listing.description|linebreaks }}</p>
8
-  <h2 class="font-semibold mt-6 mb-2">Items</h2>
9
+  <h2 class="font-semibold mt-6 mb-2">{% trans "Items" %}</h2>
9 10
   <ul class="list-disc ml-6 text-sm text-gray-700">
10 11
   {% for it in listing.items.all %}
11 12
     <li>{{ it.material.name }} — {{ it.quantity_estimate }} {{ it.get_unit_display }}</li>
12 13
   {% empty %}
13
-    <li>No items listed</li>
14
+    <li>{% trans "No items listed" %}</li>
14 15
   {% endfor %}
15 16
   </ul>
16 17
 </article>

+ 4 - 4
public_frontend/templates/public_frontend/listings_list.html

@@ -1,19 +1,19 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Listings{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Listings" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="grid md:grid-cols-3 gap-4">
5 6
   {% for l in listings %}
6 7
     <a href="{% url 'public_frontend:listing_detail' l.id %}" class="block p-4 border rounded bg-white hover:bg-gray-50">
7
-      <div class="text-sm text-gray-500">Ends {{ l.ends_at|default:'—' }}</div>
8
+      <div class="text-sm text-gray-500">{% trans "Ends" %} {{ l.ends_at|default:'—' }}</div>
8 9
       <div class="font-medium">{{ l.title }}</div>
9 10
       <div class="text-sm text-gray-600 line-clamp-2">{{ l.description|truncatechars:140 }}</div>
10 11
     </a>
11 12
   {% empty %}
12
-    <div class="text-gray-500">No public listings.</div>
13
+    <div class="text-gray-500">{% trans "No public listings." %}</div>
13 14
   {% endfor %}
14 15
 </div>
15 16
 <div class="mt-4">
16 17
   {% include 'admin_frontend/_pagination.html' %}
17 18
   </div>
18 19
 {% endblock %}
19
-

+ 6 - 5
public_frontend/templates/public_frontend/materials_list.html

@@ -1,13 +1,14 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Materials{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Materials" %}{% endblock %}
3 4
 {% block content %}
4 5
 <div class="bg-white rounded shadow overflow-hidden">
5 6
   <table class="min-w-full text-sm">
6 7
     <thead class="bg-gray-50 text-left">
7 8
       <tr>
8
-        <th class="px-4 py-2">Name</th>
9
-        <th class="px-4 py-2">Category</th>
10
-        <th class="px-4 py-2">Default Unit</th>
9
+        <th class="px-4 py-2">{% trans "Name" %}</th>
10
+        <th class="px-4 py-2">{% trans "Category" %}</th>
11
+        <th class="px-4 py-2">{% trans "Default Unit" %}</th>
11 12
       </tr>
12 13
     </thead>
13 14
     <tbody>
@@ -18,7 +19,7 @@
18 19
           <td class="px-4 py-2">{{ m.get_default_unit_display }}</td>
19 20
         </tr>
20 21
       {% empty %}
21
-        <tr><td colspan="3" class="px-4 py-6 text-center text-gray-500">No materials published.</td></tr>
22
+        <tr><td colspan="3" class="px-4 py-6 text-center text-gray-500">{% trans "No materials published." %}</td></tr>
22 23
       {% endfor %}
23 24
     </tbody>
24 25
   </table>

+ 15 - 14
public_frontend/templates/public_frontend/pickup_request.html

@@ -1,8 +1,9 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
-{% block title %}Request Pickup{% endblock %}
2
+{% load i18n %}
3
+{% block title %}{% trans "Request Pickup" %}{% endblock %}
3 4
 {% load crispy_forms_tags widget_tweaks %}
4 5
 {% block content %}
5
-<h1 class="text-xl font-semibold mb-4">Request a Pickup</h1>
6
+<h1 class="text-xl font-semibold mb-4">{% trans "Request a Pickup" %}</h1>
6 7
 <div x-data="pickupForm()">
7 8
   <div class="bg-white rounded shadow p-4" >
8 9
     <form x-ref="form" id="pickup-form" method="post" enctype="multipart/form-data" class="space-y-4">
@@ -32,33 +33,33 @@
32 33
 
33 34
   <dialog x-ref="dialog" id="review_modal" class="modal dark:bg-black/40">
34 35
     <div class="modal-box dark:bg-gray-900 dark:text-gray-100 dark:border dark:border-gray-700">
35
-      <h3 class="font-bold text-lg">Review Your Pickup Request</h3>
36
+      <h3 class="font-bold text-lg">{% trans "Review Your Pickup Request" %}</h3>
36 37
       <div class="py-4 space-y-3">
37
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Name</div><div class="font-medium" x-text="rv.name || '—'"></div></div>
38
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Name" %}</div><div class="font-medium" x-text="rv.name || '—'"></div></div>
38 39
         <div class="grid md:grid-cols-2 gap-4">
39
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Email</div><div class="font-medium" x-text="rv.email || '—'"></div></div>
40
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Phone</div><div class="font-medium" x-text="rv.phone || '—'"></div></div>
40
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Email" %}</div><div class="font-medium" x-text="rv.email || '—'"></div></div>
41
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Phone" %}</div><div class="font-medium" x-text="rv.phone || '—'"></div></div>
41 42
         </div>
42
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Pickup Address</div><div class="whitespace-pre-line" x-text="rv.address || '—'"></div></div>
43
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Pickup Address" %}</div><div class="whitespace-pre-line" x-text="rv.address || '—'"></div></div>
43 44
         <div class="grid md:grid-cols-2 gap-4">
44
-          <div><div class="text-sm text-gray-600 dark:text-gray-300">Preferred Date/Time</div><div class="font-medium" x-text="rv.preferred_at || '—'"></div></div>
45
+          <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Preferred Date/Time" %}</div><div class="font-medium" x-text="rv.preferred_at || '—'"></div></div>
45 46
         </div>
46
-        <div><div class="text-sm text-gray-600 dark:text-gray-300">Materials / Notes</div><div class="whitespace-pre-line" x-text="rv.materials || '—'"></div></div>
47
+        <div><div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Materials / Notes" %}</div><div class="whitespace-pre-line" x-text="rv.materials || '—'"></div></div>
47 48
         <div>
48
-          <div class="text-sm text-gray-600 dark:text-gray-300">Photos</div>
49
+          <div class="text-sm text-gray-600 dark:text-gray-300">{% trans "Photos" %}</div>
49 50
           <ul class="list-disc list-inside text-sm text-gray-700 dark:text-gray-300">
50
-            <template x-if="(rv.photos||[]).length === 0"><li>No files attached.</li></template>
51
+            <template x-if="(rv.photos||[]).length === 0"><li>{% trans "No files attached." %}</li></template>
51 52
             <template x-for="fname in rv.photos" :key="fname"><li x-text="fname"></li></template>
52 53
           </ul>
53 54
         </div>
54 55
       </div>
55 56
       <div class="modal-action">
56
-        <button class="btn" @click="$refs.dialog.close()">Edit</button>
57
-        <button class="btn btn-primary" @click.prevent="confirm">Confirm &amp; Send</button>
57
+        <button class="btn" @click="$refs.dialog.close()">{% trans "Edit" %}</button>
58
+        <button class="btn btn-primary" @click.prevent="confirm">{% trans "Confirm & Send" %}</button>
58 59
       </div>
59 60
     </div>
60 61
     <form method="dialog" class="modal-backdrop dark:bg-black/60">
61
-      <button class="dark:text-gray-200">close</button>
62
+      <button class="dark:text-gray-200">{% trans "close" %}</button>
62 63
     </form>
63 64
   </dialog>
64 65
 </div>

+ 2 - 2
public_frontend/templates/public_frontend/service_detail.html

@@ -1,4 +1,5 @@
1 1
 {% extends 'public_frontend/base.html' %}
2
+{% load i18n %}
2 3
 {% load static %}
3 4
 {% block title %}{{ service.title }}{% endblock %}
4 5
 
@@ -25,8 +26,7 @@
25 26
   {% endif %}
26 27
 
27 28
   <div>
28
-    <a href="/" class="text-blue-700 hover:underline">← Back to Home</a>
29
+    <a href="/" class="text-blue-700 hover:underline">← {% trans "Back to Home" %}</a>
29 30
   </div>
30 31
 </article>
31 32
 {% endblock %}
32
-

+ 9 - 8
recycle_core/models.py

@@ -6,6 +6,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey
6 6
 from django.contrib.contenttypes.models import ContentType
7 7
 from django.db import models
8 8
 from django.utils import timezone
9
+from django.utils.translation import gettext_lazy as _
9 10
 from orgs.models import Organization
10 11
 from markdownfield.models import MarkdownField, RenderedMarkdownField
11 12
 from markdownfield.validators import VALIDATOR_STANDARD
@@ -52,14 +53,14 @@ class MaterialCategory(TimestampedModel):
52 53
 class ProvidedService(TimestampedModel):
53 54
     organization = models.ForeignKey(Organization, on_delete=models.CASCADE, related_name="services")
54 55
     TITLE_CHOICES = (
55
-        ("Pickup & Logistics", "Pickup & Logistics"),
56
-        ("Material Sorting", "Material Sorting"),
57
-        ("Weighing & Ticketing", "Weighing & Ticketing"),
58
-        ("Invoicing & Payouts", "Invoicing & Payouts"),
59
-        ("Reporting & Analytics", "Reporting & Analytics"),
60
-        ("Marketplace & Bidding", "Marketplace & Bidding"),
61
-        ("Compliance & Audits", "Compliance & Audits"),
62
-        ("Consulting & Training", "Consulting & Training"),
56
+        ("Pickup & Logistics", _("Pickup & Logistics")),
57
+        ("Material Sorting", _("Material Sorting")),
58
+        ("Weighing & Ticketing", _("Weighing & Ticketing")),
59
+        ("Invoicing & Payouts", _("Invoicing & Payouts")),
60
+        ("Reporting & Analytics", _("Reporting & Analytics")),
61
+        ("Marketplace & Bidding", _("Marketplace & Bidding")),
62
+        ("Compliance & Audits", _("Compliance & Audits")),
63
+        ("Consulting & Training", _("Consulting & Training")),
63 64
     )
64 65
     title = models.CharField(max_length=100, choices=TITLE_CHOICES)
65 66
     # Short summary text

+ 10 - 8
recycle_core/templates/recycle_core/pickups_list.html

@@ -8,9 +8,9 @@
8 8
 </div>
9 9
 
10 10
 <div class="bg-white rounded shadow p-4 mb-4">
11
-  <form method="get" class="space-y-3 flex gap-2">
11
+  <form method="get" class="flex flex-wrap gap-3 items-end">
12 12
     {{ filter.form|crispy }}
13
-    <div class="flex items-center gap-2">
13
+    <div class="flex items-center gap-2 w-full">
14 14
       <button class="px-3 py-2 bg-blue-600 text-white rounded">Filter</button>
15 15
       <a href="{% url 'recycle_core:pickups_list' %}" class="btn-outline">Reset</a>
16 16
     </div>
@@ -20,15 +20,16 @@
20 20
   {% endif %}
21 21
 </div>
22 22
 
23
-<div class="bg-white rounded shadow overflow-hidden">
24
-  <table class="min-w-full divide-y divide-gray-200">
23
+<div class="bg-white rounded shadow">
24
+  <div class="overflow-x-auto">
25
+  <table class="min-w-[1100px] divide-y divide-gray-200">
25 26
     <thead class="bg-gray-50">
26 27
       <tr>
27 28
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Customer</th>
28 29
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Site</th>
29 30
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Status</th>
30 31
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Scheduled</th>
31
-        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Driver</th>
32
+        <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 w-56 min-w-[220px]">Driver</th>
32 33
         <th class="px-4 py-2 text-left text-xs font-medium text-gray-500">Actions</th>
33 34
       </tr>
34 35
     </thead>
@@ -38,11 +39,11 @@
38 39
         <td class="px-4 py-2">{{ p.customer.name }}</td>
39 40
         <td class="px-4 py-2">{{ p.site.name }}</td>
40 41
         <td class="px-4 py-2"><span class="px-2 py-1 rounded bg-gray-100 text-gray-700">{{ p.status }}</span></td>
41
-        <td class="px-4 py-2 text-gray-500">{{ p.scheduled_at|default:"-" }}</td>
42
-        <td class="px-4 py-2 text-gray-600">{{ p.assigned_driver.username|default:"-" }}</td>
42
+        <td class="px-4 py-2 text-gray-500">{{ p.scheduled_at|date:"SHORT_DATETIME_FORMAT"|default:"-" }}</td>
43
+        <td class="px-4 py-2 text-gray-600 w-56 min-w-[220px] whitespace-nowrap">{{ p.assigned_driver.username|default:"-" }}</td>
43 44
         <td class="px-4 py-2">
44 45
           <div class="flex flex-wrap gap-2 justify-end">
45
-            <a class="btn-outline btn-xs" href="{% url 'recycle_core:pickup_detail' p.id %}">View</a>
46
+            <a class="text-blue-600 hover:text-blue-700 hover:underline text-xs font-medium mr-3" href="{% url 'recycle_core:pickup_detail' p.id %}">View</a>
46 47
             {% if perms.recycle_core.assign_driver %}
47 48
             <form method="post" action="{% url 'recycle_core:pickup_assign' p.id %}" class="flex items-center gap-2">
48 49
               {% csrf_token %}
@@ -78,6 +79,7 @@
78 79
       {% endfor %}
79 80
     </tbody>
80 81
   </table>
82
+  </div>
81 83
   {% include 'admin_frontend/_pagination.html' %}
82 84
 </div>
83 85
 {% endblock %}

+ 22 - 1
recycle_core/views.py

@@ -7,6 +7,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
7 7
 import django_filters as filters
8 8
 
9 9
 from admin_frontend.templatetags.public_urls import public_route
10
+from django import forms
10 11
 from admin_frontend.nav import _nav_items
11 12
 from cms.views import breadcrumbs
12 13
 from orgs.decorators import permissions_required
@@ -421,7 +422,16 @@ def pickups_list(request):
421 422
         site = filters.ModelChoiceFilter(queryset=CustomerSite.objects.all())
422 423
         assigned_driver = filters.CharFilter(field_name="assigned_driver__username", lookup_expr="icontains", label="Driver")
423 424
         status = filters.ChoiceFilter(field_name="status", choices=PickupOrder.STATUS_CHOICES)
424
-        scheduled_at = filters.DateFromToRangeFilter(field_name="scheduled_at", label="Scheduled between")
425
+        scheduled_at = filters.DateTimeFromToRangeFilter(
426
+            field_name="scheduled_at",
427
+            label="Scheduled between",
428
+            widget=filters.widgets.RangeWidget(
429
+                attrs={
430
+                    "type": "datetime-local",
431
+                    "class": "border border-gray-300 rounded px-2 py-1"
432
+                }
433
+            ),
434
+        )
425 435
 
426 436
         class Meta:
427 437
             model = PickupOrder
@@ -448,6 +458,11 @@ def pickups_list(request):
448 458
 
449 459
     # empty forms used in row actions
450 460
     assign_form = PickupAssignForm()
461
+    # Limit driver choices to users with driver role, scoped to org if present
462
+    drivers_qs = get_user_model().objects.filter(recycle_profile__role="driver")
463
+    if org is not None:
464
+        drivers_qs = drivers_qs.filter(recycle_profile__organization=org)
465
+    assign_form.fields["driver"].queryset = drivers_qs.order_by("username")
451 466
     status_form = PickupStatusForm()
452 467
 
453 468
     context = {
@@ -467,6 +482,12 @@ def pickups_list(request):
467 482
 def pickup_assign(request, pk: int):
468 483
     pickup = get_object_or_404(PickupOrder, pk=pk)
469 484
     form = PickupAssignForm(request.POST)
485
+    # Enforce driver role (and org, if present) on POST validation
486
+    org = getattr(request, "org", None)
487
+    drivers_qs = get_user_model().objects.filter(recycle_profile__role="driver")
488
+    if org is not None:
489
+        drivers_qs = drivers_qs.filter(recycle_profile__organization=org)
490
+    form.fields["driver"].queryset = drivers_qs
470 491
     if form.is_valid():
471 492
         pickup.assigned_driver = form.cleaned_data["driver"]
472 493
         pickup.status = PickupOrder.STATUS_SCHEDULED

+ 27 - 0
translations/menu_labels.py

@@ -0,0 +1,27 @@
1
+from django.utils.translation import gettext_lazy as _
2
+
3
+# Mark app menu labels for translation extraction
4
+_("Dashboard")
5
+_("Profiles")
6
+_("Opportunities")
7
+_("Intro Requests")
8
+_("Leaderboard")
9
+_("Leads")
10
+_("CMS Posts")
11
+_("CMS Categories")
12
+_("Settings")
13
+_("Advanced Settings")
14
+_("Organization Settings")
15
+_("Organization Sites")
16
+_("Permissions")
17
+_("Users")
18
+_("Materials")
19
+_("Customers")
20
+_("Pickups")
21
+_("Weigh Tickets")
22
+_("Documents")
23
+_("Provided Services")
24
+_("Scrap Listings")
25
+_("Invoices")
26
+_("Payouts")
27
+

tum/kaceeapp7 - Gogs: Simplico Git Service

暂无描述

gradle.properties 1.1KB

12345678910111213141516171819202122232425
  1. # Project-wide Gradle settings.
  2. # IDE (e.g. Android Studio) users:
  3. # Gradle settings configured through the IDE *will override*
  4. # any settings specified in this file.
  5. # For more details on how to configure your build environment visit
  6. # http://www.gradle.org/docs/current/userguide/build_environment.html
  7. # Specifies the JVM arguments used for the daemon process.
  8. # The setting is particularly useful for tweaking memory settings.
  9. org.gradle.jvmargs=-Xmx1536m
  10. # When configured, Gradle will run in incubating parallel mode.
  11. # This option should only be used with decoupled projects. More details, visit
  12. # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
  13. # org.gradle.parallel=true
  14. # AndroidX package structure to make it clearer which packages are bundled with the
  15. # Android operating system, and which are packaged with your app's APK
  16. # https://developer.android.com/topic/libraries/support-library/androidx-rn
  17. android.useAndroidX=true
  18. # Automatically convert third-party libraries to use AndroidX
  19. android.enableJetifier=true