23">223
+msgstr "回収住所"
224
+
225
+#: public_frontend/templates/public_frontend/pickup_request.html:45
226
+msgid "Preferred Date/Time"
227
+msgstr "希望日時"
228
+
229
+#: public_frontend/templates/public_frontend/pickup_request.html:47
230
+msgid "Materials / Notes"
231
+msgstr "素材/メモ"
232
+
233
+#: public_frontend/templates/public_frontend/pickup_request.html:49
234
+msgid "Photos"
235
+msgstr "写真"
236
+
237
+#: public_frontend/templates/public_frontend/pickup_request.html:51
238
+msgid "No files attached."
239
+msgstr "添付ファイルはありません。"
240
+
241
+#: public_frontend/templates/public_frontend/pickup_request.html:57
242
+msgid "Edit"
243
+msgstr "編集"
244
+
245
+#: public_frontend/templates/public_frontend/pickup_request.html:58
246
+msgid "Confirm & Send"
247
+msgstr "確認して送信"
248
+
249
+#: public_frontend/templates/public_frontend/pickup_request.html:62
250
+msgid "close"
251
+msgstr "閉じる"
252
+
253
+#: public_frontend/templates/public_frontend/service_detail.html:29
254
+msgid "Back to Home"
255
+msgstr "ホームに戻る"
256
+
257
+#: translations/menu_labels.py:4
258
+msgid "Dashboard"
259
+msgstr "ダッシュボード"
260
+
261
+#: translations/menu_labels.py:5
262
+msgid "Profiles"
263
+msgstr "プロフィール"
264
+
265
+#: translations/menu_labels.py:6
266
+msgid "Opportunities"
267
+msgstr "案件"
268
+
269
+#: translations/menu_labels.py:7
270
+msgid "Intro Requests"
271
+msgstr "紹介リクエスト"
272
+
273
+#: translations/menu_labels.py:8
274
+msgid "Leaderboard"
275
+msgstr "ランキング"
276
+
277
+#: translations/menu_labels.py:9
278
+msgid "Leads"
279
+msgstr "リード"
280
+
281
+#: translations/menu_labels.py:10
282
+msgid "CMS Posts"
283
+msgstr "CMS投稿"
284
+
285
+#: translations/menu_labels.py:11
286
+msgid "CMS Categories"
287
+msgstr "CMSカテゴリ"
288
+
289
+#: translations/menu_labels.py:13
290
+msgid "Advanced Settings"
291
+msgstr "高度な設定"
292
+
293
+#: translations/menu_labels.py:14
294
+msgid "Organization Settings"
295
+msgstr "組織設定"
296
+
297
+#: translations/menu_labels.py:15
298
+msgid "Organization Sites"
299
+msgstr "組織サイト"
300
+
301
+#: translations/menu_labels.py:16
302
+msgid "Permissions"
303
+msgstr "権限"
304
+
305
+#: translations/menu_labels.py:17
306
+msgid "Users"
307
+msgstr "ユーザー"
308
+
309
+#: translations/menu_labels.py:19
310
+msgid "Customers"
311
+msgstr "顧客"
312
+
313
+#: translations/menu_labels.py:20
314
+msgid "Pickups"
315
+msgstr "回収"
316
+
317
+#: translations/menu_labels.py:21
318
+msgid "Weigh Tickets"
319
+msgstr "計量票"
320
+
321
+#: translations/menu_labels.py:22
322
+msgid "Documents"
323
+msgstr "ドキュメント"
324
+
325
+#: translations/menu_labels.py:23
326
+msgid "Provided Services"
327
+msgstr "提供サービス"
328
+
329
+#: translations/menu_labels.py:24
330
+msgid "Scrap Listings"
331
+msgstr "スクラップ出品"
332
+
333
+#: translations/menu_labels.py:25
334
+msgid "Invoices"
335
+msgstr "請求書"
336
+
337
+#: translations/menu_labels.py:26
338
+msgid "Payouts"
339
+msgstr "支払い"
340
+
341
+#: recycle_core/models.py
342
+msgid "Pickup & Logistics"
343
+msgstr "回収と物流"
344
+
345
+#: recycle_core/models.py
346
+msgid "Material Sorting"
347
+msgstr "素材の仕分け"
348
+
349
+#: recycle_core/models.py
350
+msgid "Weighing & Ticketing"
351
+msgstr "計量とチケット処理"
352
+
353
+#: recycle_core/models.py
354
+msgid "Invoicing & Payouts"
355
+msgstr "請求と支払い"
356
+
357
+#: recycle_core/models.py
358
+msgid "Reporting & Analytics"
359
+msgstr "レポートと分析"
360
+
361
+#: recycle_core/models.py
362
+msgid "Marketplace & Bidding"
363
+msgstr "マーケットプレイスと入札"
364
+
365
+#: recycle_core/models.py
366
+msgid "Compliance & Audits"
367
+msgstr "コンプライアンスと監査"
368
+
369
+#: recycle_core/models.py
370
+msgid "Consulting & Training"
371
+msgstr "コンサルティングとトレーニング"

BIN
locale/th/LC_MESSAGES/django.mo


+ 370 - 0
locale/th/LC_MESSAGES/django.po

@@ -0,0 +1,370 @@
1
+# SOME DESCRIPTIVE TITLE.
2
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
3
+# This file is distributed under the same license as the PACKAGE package.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
5
+#
6
+msgid ""
7
+msgstr ""
8
+"Project-Id-Version: PACKAGE VERSION\n"
9
+"Report-Msgid-Bugs-To: \n"
10
+"POT-Creation-Date: 2025-09-25 22:51+0700\n"
11
+"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
12
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
13
+"Language-Team: LANGUAGE <LL@li.org>\n"
14
+"Language: th\n"
15
+"MIME-Version: 1.0\n"
16
+"Content-Type: text/plain; charset=UTF-8\n"
17
+"Content-Transfer-Encoding: 8bit\n"
18
+"Plural-Forms: nplurals=1; plural=0;\n"
19
+
20
+#: admin_frontend/templates/admin_frontend/base.html:76
21
+#: translations/menu_labels.py:12
22
+msgid "Settings"
23
+msgstr "การตั้งค่า"
24
+
25
+#: admin_frontend/templates/admin_frontend/base.html:96
26
+msgid "CMS"
27
+msgstr "CMS"
28
+
29
+#: admin_frontend/templates/admin_frontend/base.html:117
30
+msgid "Frontend"
31
+msgstr "ส่วนหน้า"
32
+
33
+#: admin_frontend/templates/admin_frontend/base.html:137
34
+msgid "Organization"
35
+msgstr "องค์กร"
36
+
37
+#: admin_frontend/templates/admin_frontend/base.html:157
38
+msgid "Billing"
39
+msgstr "การเรียกเก็บเงิน"
40
+
41
+#: admin_frontend/templates/admin_frontend/base.html:177
42
+msgid "Recycle Ops"
43
+msgstr "การดำเนินงานรีไซเคิล"
44
+
45
+#: public_frontend/templates/public_frontend/base.html:17
46
+#: public_frontend/templates/public_frontend/materials_list.html:3
47
+#: translations/menu_labels.py:18
48
+msgid "Materials"
49
+msgstr "วัสดุ"
50
+
51
+#: public_frontend/templates/public_frontend/base.html:18
52
+#: public_frontend/templates/public_frontend/listings_list.html:3
53
+msgid "Listings"
54
+msgstr "รายการ"
55
+
56
+#: public_frontend/templates/public_frontend/base.html:19
57
+#: public_frontend/templates/public_frontend/pickup_request.html:3
58
+msgid "Request Pickup"
59
+msgstr "ขอรับของ"
60
+
61
+#: public_frontend/templates/public_frontend/base.html:20
62
+#: public_frontend/templates/public_frontend/blog_list.html:3
63
+msgid "Blog"
64
+msgstr "บล็อก"
65
+
66
+#: public_frontend/templates/public_frontend/base.html:21
67
+#: public_frontend/templates/public_frontend/contact.html:3
68
+msgid "Contact"
69
+msgstr "ติดต่อเรา"
70
+
71
+#: public_frontend/templates/public_frontend/base.html:53
72
+msgid "Staff Login"
73
+msgstr "เข้าสู่ระบบเจ้าหน้าที่"
74
+
75
+#: public_frontend/templates/public_frontend/blog_list.html:14
76
+msgid "No image"
77
+msgstr "ไม่มีรูปภาพ"
78
+
79
+#: public_frontend/templates/public_frontend/blog_list.html:24
80
+#: public_frontend/templates/public_frontend/home.html:92
81
+msgid "No posts yet."
82
+msgstr "ยังไม่มีโพสต์"
83
+
84
+#: public_frontend/templates/public_frontend/contact.html:5
85
+#: public_frontend/templates/public_frontend/home.html:99
86
+msgid "Contact Us"
87
+msgstr "ติดต่อเรา"
88
+
89
+#: public_frontend/templates/public_frontend/contact.html:10
90
+#: public_frontend/templates/public_frontend/home.html:104
91
+#: public_frontend/templates/public_frontend/materials_list.html:9
92
+#: public_frontend/templates/public_frontend/pickup_request.html:38
93
+msgid "Name"
94
+msgstr "ชื่อ"
95
+
96
+#: public_frontend/templates/public_frontend/contact.html:14
97
+#: public_frontend/templates/public_frontend/home.html:108
98
+#: public_frontend/templates/public_frontend/pickup_request.html:40
99
+msgid "Email"
100
+msgstr "อีเมล"
101
+
102
+#: public_frontend/templates/public_frontend/contact.html:20
103
+#: public_frontend/templates/public_frontend/pickup_request.html:41
104
+msgid "Phone"
105
+msgstr "โทรศัพท์"
106
+
107
+#: public_frontend/templates/public_frontend/contact.html:24
108
+#: public_frontend/templates/public_frontend/home.html:113
109
+msgid "Subject"
110
+msgstr "หัวข้อ"
111
+
112
+#: public_frontend/templates/public_frontend/contact.html:29
113
+#: public_frontend/templates/public_frontend/home.html:117
114
+msgid "Message"
115
+msgstr "ข้อความ"
116
+
117
+#: public_frontend/templates/public_frontend/contact.html:33
118
+msgid "Send"
119
+msgstr "ส่ง"
120
+
121
+#: public_frontend/templates/public_frontend/home.html:3
122
+msgid "Home"
123
+msgstr "หน้าแรก"
124
+
125
+#: public_frontend/templates/public_frontend/home.html:9
126
+msgid "Streamline Your Factory's Recycling"
127
+msgstr "ปรับปรุงการรีไซเคิลของโรงงานให้มีประสิทธิภาพ"
128
+
129
+#: public_frontend/templates/public_frontend/home.html:10
130
+msgid ""
131
+"We partner with businesses like yours to manage scrap materials efficiently. "
132
+"Browse listings, request a pickup, and turn your waste into a resource."
133
+msgstr "เราร่วมมือกับธุรกิจเช่นคุณเพื่อจัดการเศษวัสดุอย่างมีประสิทธิภาพ เรียกดูรายการ ขอรับของ และเปลี่ยนของเสียให้เป็นทรัพยากร"
134
+
135
+#: public_frontend/templates/public_frontend/home.html:12
136
+#: public_frontend/templates/public_frontend/pickup_request.html:6
137
+msgid "Request a Pickup"
138
+msgstr "ขอรับของ"
139
+
140
+#: public_frontend/templates/public_frontend/home.html:13
141
+msgid "Browse Listings"
142
+msgstr "ดูรายการ"
143
+
144
+#: public_frontend/templates/public_frontend/home.html:21
145
+msgid "Our Services"
146
+msgstr "บริการของเรา"
147
+
148
+#: public_frontend/templates/public_frontend/home.html:40
149
+msgid "Materials We Accept"
150
+msgstr "วัสดุที่รับ"
151
+
152
+#: public_frontend/templates/public_frontend/home.html:41
153
+#: public_frontend/templates/public_frontend/home.html:82
154
+msgid "View all"
155
+msgstr "ดูทั้งหมด"
156
+
157
+#: public_frontend/templates/public_frontend/home.html:51
158
+#: public_frontend/templates/public_frontend/materials_list.html:22
159
+msgid "No materials published."
160
+msgstr "ยังไม่มีวัสดุเผยแพร่"
161
+
162
+#: public_frontend/templates/public_frontend/home.html:60
163
+msgid "Open Listings"
164
+msgstr "รายการที่เปิดอยู่"
165
+
166
+#: public_frontend/templates/public_frontend/home.html:61
167
+msgid "Browse all"
168
+msgstr "ดูทั้งหมด"
169
+
170
+#: public_frontend/templates/public_frontend/home.html:66
171
+#: public_frontend/templates/public_frontend/listing_detail.html:7
172
+#: public_frontend/templates/public_frontend/listings_list.html:8
173
+msgid "Ends"
174
+msgstr "สิ้นสุด"
175
+
176
+#: public_frontend/templates/public_frontend/home.html:71
177
+msgid "No public listings at the moment."
178
+msgstr "ขณะนี้ยังไม่มีรายการสาธารณะ"
179
+
180
+#: public_frontend/templates/public_frontend/home.html:81
181
+msgid "Industry Insights"
182
+msgstr "ข้อมูลเชิงลึกอุตสาหกรรม"
183
+
184
+#: public_frontend/templates/public_frontend/home.html:121
185
+msgid "Send Message"
186
+msgstr "ส่งข้อความ"
187
+
188
+#: public_frontend/templates/public_frontend/listing_detail.html:3
189
+msgid "Listing"
190
+msgstr "รายการ"
191
+
192
+#: public_frontend/templates/public_frontend/listing_detail.html:7
193
+msgid "Status"
194
+msgstr "สถานะ"
195
+
196
+#: public_frontend/templates/public_frontend/listing_detail.html:9
197
+msgid "Items"
198
+msgstr "รายการ"
199
+
200
+#: public_frontend/templates/public_frontend/listing_detail.html:14
201
+msgid "No items listed"
202
+msgstr "ยังไม่มีรายการ"
203
+
204
+#: public_frontend/templates/public_frontend/listings_list.html:13
205
+msgid "No public listings."
206
+msgstr "ยังไม่มีรายการสาธารณะ"
207
+
208
+#: public_frontend/templates/public_frontend/materials_list.html:10
209
+msgid "Category"
210
+msgstr "หมวดหมู่"
211
+
212
+#: public_frontend/templates/public_frontend/materials_list.html:11
213
+msgid "Default Unit"
214
+msgstr "หน่วยเริ่มต้น"
215
+
216
+#: public_frontend/templates/public_frontend/pickup_request.html:36
217
+msgid "Review Your Pickup Request"
218
+msgstr "ตรวจสอบคำขอรับของของคุณ"
219
+
220
+#: public_frontend/templates/public_frontend/pickup_request.html:43
221
+msgid "Pickup Address"
222
+msgstr "ที่อยู่รับของ"
223
+
224
+#: public_frontend/templates/public_frontend/pickup_request.html:45
225
+msgid "Preferred Date/Time"
226
+msgstr "วันที่/เวลา ที่ต้องการ"
227
+
228
+#: public_frontend/templates/public_frontend/pickup_request.html:47
229
+msgid "Materials / Notes"
230
+msgstr "วัสดุ / หมายเหตุ"
231
+
232
+#: public_frontend/templates/public_frontend/pickup_request.html:49
233
+msgid "Photos"
234
+msgstr "รูปภาพ"
235
+
236
+#: public_frontend/templates/public_frontend/pickup_request.html:51
237
+msgid "No files attached."
238
+msgstr "ไม่มีไฟล์แนบ"
239
+
240
+#: public_frontend/templates/public_frontend/pickup_request.html:57
241
+msgid "Edit"
242
+msgstr "แก้ไข"
243
+
244
+#: public_frontend/templates/public_frontend/pickup_request.html:58
245
+msgid "Confirm & Send"
246
+msgstr "ยืนยันและส่ง"
247
+
248
+#: public_frontend/templates/public_frontend/pickup_request.html:62
249
+msgid "close"
250
+msgstr "ปิด"
251
+
252
+#: public_frontend/templates/public_frontend/service_detail.html:29
253
+msgid "Back to Home"
254
+msgstr "กลับสู่หน้าแรก"
255
+
256
+#: translations/menu_labels.py:4
257
+msgid "Dashboard"
258
+msgstr "แดชบอร์ด"
259
+
260
+#: translations/menu_labels.py:5
261
+msgid "Profiles"
262
+msgstr "โปรไฟล์"
263
+
264
+#: translations/menu_labels.py:6
265
+msgid "Opportunities"
266
+msgstr "โอกาส"
267
+
268
+#: translations/menu_labels.py:7
269
+msgid "Intro Requests"
270
+msgstr "คำขอแนะนำ"
271
+
272
+#: translations/menu_labels.py:8
273
+msgid "Leaderboard"
274
+msgstr "ตารางจัดอันดับ"
275
+
276
+#: translations/menu_labels.py:9
277
+msgid "Leads"
278
+msgstr "ลีด"
279
+
280
+#: translations/menu_labels.py:10
281
+msgid "CMS Posts"
282
+msgstr "โพสต์ CMS"
283
+
284
+#: translations/menu_labels.py:11
285
+msgid "CMS Categories"
286
+msgstr "หมวดหมู่ CMS"
287
+
288
+#: translations/menu_labels.py:13
289
+msgid "Advanced Settings"
290
+msgstr "การตั้งค่าขั้นสูง"
291
+
292
+#: translations/menu_labels.py:14
293
+msgid "Organization Settings"
294
+msgstr "การตั้งค่าองค์กร"
295
+
296
+#: translations/menu_labels.py:15
297
+msgid "Organization Sites"
298
+msgstr "ไซต์ขององค์กร"
299
+
300
+#: translations/menu_labels.py:16
301
+msgid "Permissions"
302
+msgstr "สิทธิ์การใช้งาน"
303
+
304
+#: translations/menu_labels.py:17
305
+msgid "Users"
306
+msgstr "ผู้ใช้"
307
+
308
+#: translations/menu_labels.py:19
309
+msgid "Customers"
310
+msgstr "ลูกค้า"
311
+
312
+#: translations/menu_labels.py:20
313
+msgid "Pickups"
314
+msgstr "การรับของ"
315
+
316
+#: translations/menu_labels.py:21
317
+msgid "Weigh Tickets"
318
+msgstr "ใบชั่งน้ำหนัก"
319
+
320
+#: translations/menu_labels.py:22
321
+msgid "Documents"
322
+msgstr "เอกสาร"
323
+
324
+#: translations/menu_labels.py:23
325
+msgid "Provided Services"
326
+msgstr "บริการที่ให้"
327
+
328
+#: translations/menu_labels.py:24
329
+msgid "Scrap Listings"
330
+msgstr "รายการเศษวัสดุ"
331
+
332
+#: translations/menu_labels.py:25
333
+msgid "Invoices"
334
+msgstr "ใบแจ้งหนี้"
335
+
336
+#: translations/menu_labels.py:26
337
+msgid "Payouts"
338
+msgstr "การจ่ายเงิน"
339
+
340
+#: recycle_core/models.py
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/network_report_server - Gogs: Simplico Git Service

Keine Beschreibung

LICENSE 2.1KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. The ISC License
  2. Copyright (c) Isaac Z. Schlueter and Contributors
  3. Permission to use, copy, modify, and/or distribute this software for any
  4. purpose with or without fee is hereby granted, provided that the above
  5. copyright notice and this permission notice appear in all copies.
  6. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
  7. WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
  8. MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
  9. ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
  10. WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
  11. ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
  12. IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  13. ----
  14. This library bundles a version of the `fs.realpath` and `fs.realpathSync`
  15. methods from Node.js v0.10 under the terms of the Node.js MIT license.
  16. Node's license follows, also included at the header of `old.js` which contains
  17. the licensed code:
  18. Copyright Joyent, Inc. and other Node contributors.
  19. Permission is hereby granted, free of charge, to any person obtaining a
  20. copy of this software and associated documentation files (the "Software"),
  21. to deal in the Software without restriction, including without limitation
  22. the rights to use, copy, modify, merge, publish, distribute, sublicense,
  23. and/or sell copies of the Software, and to permit persons to whom the
  24. Software is furnished to do so, subject to the following conditions:
  25. The above copyright notice and this permission notice shall be included in
  26. all copies or substantial portions of the Software.
  27. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  28. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  29. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  30. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  31. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  32. FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  33. DEALINGS IN THE SOFTWARE.