django-adminflow 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. adminflow/__init__.py +1 -0
  2. adminflow/apps.py +22 -0
  3. adminflow/context_processors.py +72 -0
  4. adminflow/forms.py +89 -0
  5. adminflow/import_export.py +224 -0
  6. adminflow/static/adminflow/img/logo.png +0 -0
  7. adminflow/templates/admin/actions.html +79 -0
  8. adminflow/templates/admin/base.html +1263 -0
  9. adminflow/templates/admin/base_site.html +5 -0
  10. adminflow/templates/admin/change_form.html +315 -0
  11. adminflow/templates/admin/change_list.html +216 -0
  12. adminflow/templates/admin/change_list_results.html +211 -0
  13. adminflow/templates/admin/delete_confirmation.html +55 -0
  14. adminflow/templates/admin/delete_selected_confirmation.html +118 -0
  15. adminflow/templates/admin/edit_inline/stacked.html +85 -0
  16. adminflow/templates/admin/edit_inline/tabular.html +81 -0
  17. adminflow/templates/admin/filter.html +17 -0
  18. adminflow/templates/admin/import_export/change_list_export_item.html +8 -0
  19. adminflow/templates/admin/import_export/change_list_import_item.html +8 -0
  20. adminflow/templates/admin/import_export/export.html +199 -0
  21. adminflow/templates/admin/includes/fieldset.html +73 -0
  22. adminflow/templates/admin/includes/toast.html +102 -0
  23. adminflow/templates/admin/index.html +227 -0
  24. adminflow/templates/admin/logged_out.html +38 -0
  25. adminflow/templates/admin/login.html +144 -0
  26. adminflow/templates/admin/object_history.html +60 -0
  27. adminflow/templates/admin/pagination.html +30 -0
  28. adminflow/templates/admin/search_form.html +28 -0
  29. adminflow/templates/admin/submit_line.html +48 -0
  30. adminflow/templates/admin/verify_2fa.html +280 -0
  31. adminflow/templates/otp/email/token.html +64 -0
  32. adminflow/templates/otp/email/token.txt +11 -0
  33. adminflow/templates/registration/logged_out.html +38 -0
  34. adminflow/templates/registration/password_change_form.html +272 -0
  35. adminflow/templates/simple_history/object_history.html +186 -0
  36. adminflow/templates/simple_history/object_history_form.html +103 -0
  37. adminflow/templates/simple_history/submit_line.html +17 -0
  38. adminflow/templatetags/__init__.py +1 -0
  39. adminflow/templatetags/__pycache__/__init__.cpython-314.pyc +0 -0
  40. adminflow/templatetags/__pycache__/adminflow_tags.cpython-314.pyc +0 -0
  41. adminflow/templatetags/adminflow_tags.py +233 -0
  42. adminflow/user_admin.py +662 -0
  43. adminflow/views.py +207 -0
  44. django_adminflow-1.0.0.dist-info/METADATA +398 -0
  45. django_adminflow-1.0.0.dist-info/RECORD +47 -0
  46. django_adminflow-1.0.0.dist-info/WHEEL +5 -0
  47. django_adminflow-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,64 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Verification Code | {{ adminflow_title|default:adminflow_company|default:"AdminFlow" }}</title>
7
+ </head>
8
+ <body style="margin: 0; padding: 0; background-color: #fafafa; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; color: #171717;">
9
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="background-color: #fafafa; padding: 60px 20px;">
10
+ <tr>
11
+ <td align="center">
12
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0" style="max-width: 440px; background-color: #ffffff; border-radius: 16px; border: 1px solid #e5e5e5; padding: 40px; text-align: left; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);">
13
+ <!-- Brand Header -->
14
+ <tr>
15
+ <td style="padding-bottom: 32px; border-bottom: 1px solid #f5f5f5;">
16
+ <span style="font-size: 16px; font-weight: 700; letter-spacing: -0.3px; color: #0a0a0a;">{{ adminflow_title|default:adminflow_company|default:"AdminFlow" }}</span>
17
+ </td>
18
+ </tr>
19
+
20
+ <!-- Body Content -->
21
+ <tr>
22
+ <td style="padding-top: 32px; padding-bottom: 24px;">
23
+ <h1 style="font-size: 18px; font-weight: 600; color: #0a0a0a; margin: 0 0 12px 0; letter-spacing: -0.2px;">Verification Code</h1>
24
+ <p style="font-size: 14px; color: #525252; margin: 0; line-height: 1.5;">
25
+ Enter this code to complete your verification:
26
+ </p>
27
+ </td>
28
+ </tr>
29
+
30
+ <!-- OTP Code Container -->
31
+ <tr>
32
+ <td style="padding: 16px 0;">
33
+ <div style="background-color: #f5f5f5; border-radius: 12px; padding: 24px; text-align: center;">
34
+ <span style="font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 36px; font-weight: 700; letter-spacing: 10px; color: #0a0a0a; display: block; margin-left: 10px;">{{ token }}</span>
35
+ </div>
36
+ </td>
37
+ </tr>
38
+
39
+ <!-- Expiry & Security Notice -->
40
+ <tr>
41
+ <td style="padding-top: 16px; padding-bottom: 32px; border-bottom: 1px solid #f5f5f5;">
42
+ <p style="font-size: 12px; color: #737373; margin: 0 0 8px 0; font-weight: 500;">
43
+ • Code expires in 15 minutes.
44
+ </p>
45
+ <p style="font-size: 12px; color: #a3a3a3; margin: 0; line-height: 1.5;">
46
+ Didn't request this code? You can safely ignore this email.
47
+ </p>
48
+ </td>
49
+ </tr>
50
+
51
+ <!-- Footer -->
52
+ <tr>
53
+ <td style="padding-top: 24px;">
54
+ <p style="font-size: 11px; color: #a3a3a3; margin: 0; line-height: 1.5;">
55
+ &copy; {{ adminflow_title|default:adminflow_company|default:"AdminFlow" }}. Automated security message.
56
+ </p>
57
+ </td>
58
+ </tr>
59
+ </table>
60
+ </td>
61
+ </tr>
62
+ </table>
63
+ </body>
64
+ </html>
@@ -0,0 +1,11 @@
1
+ {{ adminflow_title|default:adminflow_company|default:"AdminFlow" }} Verification Code
2
+
3
+ Enter this code to complete your verification:
4
+
5
+ {{ token }}
6
+
7
+ • Code expires in 15 minutes.
8
+ • Didn't request this code? You can safely ignore this email.
9
+
10
+ --
11
+ {{ adminflow_title|default:adminflow_company|default:"AdminFlow" }} Automated Security
@@ -0,0 +1,38 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block bodyclass %}bg-background h-full flex items-center justify-center p-4 md:p-8{% endblock %}
5
+
6
+ {% block content %}
7
+ <main class="w-full max-w-md flex flex-col overflow-hidden rounded-2xl shadow-xl bg-surface-lowest border border-outline-variant mx-auto my-[10vh] text-center p-8 md:p-12 space-y-6">
8
+ <!-- Icon Header -->
9
+ <div class="flex justify-center">
10
+ <div class="h-16 w-16 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-inner">
11
+ <span class="material-symbols-outlined text-3xl">logout</span>
12
+ </div>
13
+ </div>
14
+
15
+ <!-- Title & Subtitle -->
16
+ <div class="space-y-2">
17
+ <h1 class="text-2xl font-extrabold text-on-surface tracking-tight">{% translate "Logged Out" %}</h1>
18
+ <p class="text-xs text-on-surface-variant leading-relaxed max-w-xs mx-auto">
19
+ {% translate "Thanks for spending some quality time with the web site today." %}
20
+ </p>
21
+ </div>
22
+
23
+ <!-- Actions -->
24
+ <div class="pt-4 space-y-3">
25
+ <a href="{% url 'admin:login' %}" class="w-full h-11 bg-primary text-white rounded-xl text-xs font-bold hover:bg-primary/95 active:scale-[0.99] transition-all shadow-md flex items-center justify-center gap-2">
26
+ <span class="material-symbols-outlined text-[18px]">login</span>
27
+ <span>{% translate "Log in again" %}</span>
28
+ </a>
29
+
30
+ {% if site_url %}
31
+ <a href="{{ site_url }}" class="w-full h-10 border border-outline-variant bg-surface hover:bg-surface-low text-on-surface-variant rounded-xl text-xs font-semibold transition-all flex items-center justify-center gap-2">
32
+ <span class="material-symbols-outlined text-[16px]">home</span>
33
+ <span>{% translate "Return to site" %}</span>
34
+ </a>
35
+ {% endif %}
36
+ </div>
37
+ </main>
38
+ {% endblock %}
@@ -0,0 +1,272 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% load i18n static %}
3
+
4
+ {% block title %}{% if form.errors %}{% translate "Error:" %} {% endif %}{% translate "Password Change" %} | {{ block.super }}{% endblock %}
5
+
6
+ {% block extrastyle %}{{ block.super }}
7
+ <style>
8
+ /* ── match fieldset.html input styling ── */
9
+ .pw-input {
10
+ width: 100%;
11
+ height: 40px;
12
+ padding: 0 40px 0 12px;
13
+ border-radius: 8px;
14
+ border: 1px solid var(--outline-variant, #e2e8f0);
15
+ background: var(--surface-lowest, #f8fafc);
16
+ font-size: 12px;
17
+ color: var(--on-surface, #0f172a);
18
+ outline: none;
19
+ transition: border-color 0.15s, box-shadow 0.15s, background 0.15s;
20
+ box-sizing: border-box;
21
+ font-family: monospace;
22
+ letter-spacing: 1.5px;
23
+ }
24
+ .pw-input:focus {
25
+ border-color: var(--primary, #6366f1);
26
+ box-shadow: 0 0 0 2px rgba(99,102,241,0.10);
27
+ background: #fff;
28
+ }
29
+ .pw-input:hover:not(:focus) {
30
+ border-color: var(--outline, #94a3b8);
31
+ }
32
+
33
+ /* Show/hide toggle inside input */
34
+ .pw-input-wrap { position: relative; }
35
+ .pw-eye {
36
+ position: absolute;
37
+ right: 10px;
38
+ top: 50%;
39
+ transform: translateY(-50%);
40
+ background: none;
41
+ border: none;
42
+ cursor: pointer;
43
+ color: var(--on-surface-variant, #94a3b8);
44
+ padding: 0;
45
+ display: flex;
46
+ align-items: center;
47
+ line-height: 1;
48
+ transition: color 0.15s;
49
+ }
50
+ .pw-eye:hover { color: var(--primary, #6366f1); }
51
+ .pw-eye .material-symbols-outlined { font-size: 16px; }
52
+
53
+ /* strength bar */
54
+ .pw-strength-bar { display:flex; gap:3px; margin-top:6px; }
55
+ .pw-segment {
56
+ height: 3px;
57
+ flex: 1;
58
+ border-radius: 99px;
59
+ background: var(--outline-variant, #e2e8f0);
60
+ transition: background 0.25s;
61
+ }
62
+ </style>
63
+ {% endblock %}
64
+
65
+ {% block breadcrumbs %}
66
+ <nav class="flex items-center gap-1.5 text-xs text-on-surface-variant/70 mb-4">
67
+ <a href="{% url 'admin:index' %}" class="hover:text-primary transition-colors flex items-center">
68
+ <span class="material-symbols-outlined text-[16px]">home</span>
69
+ </a>
70
+ <span class="material-symbols-outlined text-[14px]">chevron_right</span>
71
+ <span class="text-primary font-bold">{% translate "Password Change" %}</span>
72
+ </nav>
73
+ {% endblock %}
74
+
75
+ {% block content %}
76
+ <div class="space-y-6 max-w-2xl mx-auto w-full">
77
+
78
+ <!-- Page header (matches change_form.html pattern) -->
79
+ <div>
80
+ <h2 class="text-xl md:text-2xl font-black text-on-surface tracking-tight">
81
+ {% translate "Change Password" %}
82
+ </h2>
83
+ <p class="text-xs text-on-surface-variant mt-1">
84
+ {% translate "Enter your current password then choose a new one. Changes are saved immediately." %}
85
+ </p>
86
+ </div>
87
+
88
+ {% include "admin/includes/toast.html" %}
89
+
90
+ <form method="post" id="password-change-form" class="space-y-6">
91
+ {% csrf_token %}
92
+
93
+ {% if form.errors %}
94
+ <div class="flex items-center gap-2 p-3 rounded-lg bg-danger/10 border border-danger/20 text-danger text-xs font-semibold">
95
+ <span class="material-symbols-outlined text-sm">error</span>
96
+ {% blocktranslate count counter=form.errors.items|length %}Please correct the error below.{% plural %}Please correct the errors below.{% endblocktranslate %}
97
+ </div>
98
+ {% endif %}
99
+
100
+ <!-- Current Password -->
101
+ <fieldset class="space-y-4 border border-outline-variant bg-surface-lowest p-4 sm:p-6 rounded-xl shadow-sm">
102
+ <legend class="text-xs font-black text-on-surface uppercase tracking-wider px-2 bg-surface-lowest">
103
+ {% translate "Current Password" %}
104
+ </legend>
105
+
106
+ <div class="space-y-1.5 {% if form.old_password.errors %}text-danger{% endif %}">
107
+ <label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider" for="{{ form.old_password.id_for_label }}">
108
+ {% translate "Old Password" %}<span class="text-danger">*</span>
109
+ </label>
110
+ {% if form.old_password.errors %}
111
+ <p class="text-[11px] text-danger font-semibold flex items-center gap-1">
112
+ <span class="material-symbols-outlined text-sm">error</span>
113
+ {{ form.old_password.errors|striptags }}
114
+ </p>
115
+ {% endif %}
116
+ <div class="pw-input-wrap">
117
+ <input type="password"
118
+ name="{{ form.old_password.html_name }}"
119
+ id="{{ form.old_password.id_for_label }}"
120
+ class="pw-input"
121
+ autocomplete="current-password"
122
+ required>
123
+ <button type="button" class="pw-eye" onclick="togglePw('{{ form.old_password.id_for_label }}', this)" tabindex="-1">
124
+ <span class="material-symbols-outlined">visibility</span>
125
+ </button>
126
+ </div>
127
+ </div>
128
+ </fieldset>
129
+
130
+ <!-- New Password -->
131
+ <fieldset class="space-y-4 border border-outline-variant bg-surface-lowest p-4 sm:p-6 rounded-xl shadow-sm">
132
+ <legend class="text-xs font-black text-on-surface uppercase tracking-wider px-2 bg-surface-lowest">
133
+ {% translate "New Password" %}
134
+ </legend>
135
+
136
+ <!-- New password 1 -->
137
+ <div class="space-y-1.5 {% if form.new_password1.errors %}text-danger{% endif %}">
138
+ <label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider" for="{{ form.new_password1.id_for_label }}">
139
+ {% translate "New Password" %}<span class="text-danger">*</span>
140
+ </label>
141
+ {% if form.new_password1.errors %}
142
+ <p class="text-[11px] text-danger font-semibold flex items-center gap-1">
143
+ <span class="material-symbols-outlined text-sm">error</span>
144
+ {{ form.new_password1.errors|striptags }}
145
+ </p>
146
+ {% endif %}
147
+ <div class="pw-input-wrap">
148
+ <input type="password"
149
+ name="{{ form.new_password1.html_name }}"
150
+ id="{{ form.new_password1.id_for_label }}"
151
+ class="pw-input"
152
+ autocomplete="new-password"
153
+ required>
154
+ <button type="button" class="pw-eye" onclick="togglePw('{{ form.new_password1.id_for_label }}', this)" tabindex="-1">
155
+ <span class="material-symbols-outlined">visibility</span>
156
+ </button>
157
+ </div>
158
+ <!-- Strength bar -->
159
+ <div class="pw-strength-bar" id="pw-strength-bar" style="display:none;">
160
+ <div class="pw-segment" id="ps1"></div>
161
+ <div class="pw-segment" id="ps2"></div>
162
+ <div class="pw-segment" id="ps3"></div>
163
+ <div class="pw-segment" id="ps4"></div>
164
+ </div>
165
+ <p class="text-[10px] font-semibold" id="pw-strength-label" style="display:none;"></p>
166
+
167
+ {% if form.new_password1.help_text %}
168
+ <div class="text-[11px] text-on-surface-variant bg-surface-low border border-outline-variant/60 rounded-lg p-3 space-y-1 [&_ul]:space-y-1 [&_li]:flex [&_li]:items-baseline [&_li]:gap-1.5 [&_li]:text-[11px]">
169
+ {{ form.new_password1.help_text|safe }}
170
+ </div>
171
+ {% endif %}
172
+ </div>
173
+
174
+ <!-- New password 2 (confirm) -->
175
+ <div class="space-y-1.5 {% if form.new_password2.errors %}text-danger{% endif %}">
176
+ <label class="text-[10px] font-bold text-on-surface-variant uppercase tracking-wider" for="{{ form.new_password2.id_for_label }}">
177
+ {% translate "Confirm New Password" %}<span class="text-danger">*</span>
178
+ </label>
179
+ {% if form.new_password2.errors %}
180
+ <p class="text-[11px] text-danger font-semibold flex items-center gap-1">
181
+ <span class="material-symbols-outlined text-sm">error</span>
182
+ {{ form.new_password2.errors|striptags }}
183
+ </p>
184
+ {% endif %}
185
+ <div class="pw-input-wrap">
186
+ <input type="password"
187
+ name="{{ form.new_password2.html_name }}"
188
+ id="{{ form.new_password2.id_for_label }}"
189
+ class="pw-input"
190
+ autocomplete="new-password"
191
+ required>
192
+ <button type="button" class="pw-eye" onclick="togglePw('{{ form.new_password2.id_for_label }}', this)" tabindex="-1">
193
+ <span class="material-symbols-outlined">visibility</span>
194
+ </button>
195
+ </div>
196
+ {% if form.new_password2.help_text %}
197
+ <p class="text-[11px] text-on-surface-variant flex items-center gap-1">
198
+ <span class="material-symbols-outlined text-[13px]">info</span>
199
+ {{ form.new_password2.help_text }}
200
+ </p>
201
+ {% endif %}
202
+ </div>
203
+ </fieldset>
204
+
205
+ <!-- Submit row (matches submit_line.html pattern) -->
206
+ <div class="flex items-center justify-end gap-3 bg-surface-lowest border border-outline-variant rounded-xl px-5 py-3 shadow-sm">
207
+ <button type="submit"
208
+ class="flex items-center gap-1.5 px-4 py-2 bg-primary text-white text-xs font-bold rounded-lg shadow-sm hover:opacity-90 transition-opacity">
209
+ <span class="material-symbols-outlined text-sm">lock_reset</span>
210
+ {% translate "Update Password" %}
211
+ </button>
212
+ </div>
213
+
214
+ </form>
215
+ </div>
216
+
217
+ <script>
218
+ function togglePw(id, btn) {
219
+ var input = document.getElementById(id);
220
+ var icon = btn.querySelector('.material-symbols-outlined');
221
+ if (!input) return;
222
+ if (input.type === 'password') {
223
+ input.type = 'text';
224
+ icon.textContent = 'visibility_off';
225
+ } else {
226
+ input.type = 'password';
227
+ icon.textContent = 'visibility';
228
+ }
229
+ }
230
+
231
+ // Password strength meter
232
+ (function () {
233
+ var field = document.getElementById('{{ form.new_password1.id_for_label }}');
234
+ var bar = document.getElementById('pw-strength-bar');
235
+ var lbl = document.getElementById('pw-strength-label');
236
+ var segs = ['ps1','ps2','ps3','ps4'].map(function(id){ return document.getElementById(id); });
237
+ if (!field) return;
238
+
239
+ var levels = [
240
+ { color: '#ef4444', text: 'Weak' },
241
+ { color: '#f97316', text: 'Fair' },
242
+ { color: '#eab308', text: 'Good' },
243
+ { color: '#22c55e', text: 'Strong' },
244
+ ];
245
+
246
+ function score(v) {
247
+ var s = 0;
248
+ if (v.length >= 8) s++;
249
+ if (v.length >= 12) s++;
250
+ if (/[A-Z]/.test(v) && /[a-z]/.test(v)) s++;
251
+ if (/[0-9]/.test(v) && /[^A-Za-z0-9]/.test(v)) s++;
252
+ return Math.min(Math.max(s, 1), 4);
253
+ }
254
+
255
+ field.addEventListener('input', function () {
256
+ if (!field.value) {
257
+ bar.style.display = 'none';
258
+ lbl.style.display = 'none';
259
+ return;
260
+ }
261
+ bar.style.display = 'flex';
262
+ lbl.style.display = 'block';
263
+ var s = score(field.value);
264
+ segs.forEach(function (seg, i) {
265
+ seg.style.background = i < s ? levels[s-1].color : 'var(--outline-variant, #e2e8f0)';
266
+ });
267
+ lbl.textContent = 'Strength: ' + levels[s-1].text;
268
+ lbl.style.color = levels[s-1].color;
269
+ });
270
+ })();
271
+ </script>
272
+ {% endblock %}
@@ -0,0 +1,186 @@
1
+ {% extends "admin/base_site.html" %}
2
+ {% load i18n admin_urls %}
3
+
4
+ {% block breadcrumbs %}
5
+ <nav class="flex items-center gap-1.5 text-xs text-on-surface-variant/70 mb-4">
6
+ <a href="{% url 'admin:index' %}" class="hover:text-primary transition-colors flex items-center"><span class="material-symbols-outlined text-[16px]">home</span></a>
7
+ <span class="material-symbols-outlined text-[14px]">chevron_right</span>
8
+ <a href="{% url 'admin:app_list' app_label=opts.app_label %}" class="hover:text-primary transition-colors capitalize font-medium">{{ opts.app_config.verbose_name }}</a>
9
+ <span class="material-symbols-outlined text-[14px]">chevron_right</span>
10
+ <a href="{% url opts|admin_urlname:'changelist' %}" class="hover:text-primary transition-colors font-bold">{{ opts.verbose_name_plural|capfirst }}</a>
11
+ <span class="material-symbols-outlined text-[14px]">chevron_right</span>
12
+ <a href="{% url opts|admin_urlname:'change' object.pk|admin_urlquote %}" class="hover:text-primary transition-colors font-bold">{{ object|truncatewords:"18" }}</a>
13
+ <span class="material-symbols-outlined text-[14px]">chevron_right</span>
14
+ <span class="text-primary font-bold">{% translate 'History' %}</span>
15
+ </nav>
16
+ {% endblock %}
17
+
18
+ {% block content %}
19
+ <div class="space-y-6 max-w-4xl mx-auto w-full">
20
+ <!-- Header Block -->
21
+ <div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
22
+ <div>
23
+ <h2 class="text-xl md:text-2xl font-black text-on-surface tracking-tight">
24
+ {{ object|truncatewords:"10" }} {% translate 'History' %}
25
+ </h2>
26
+ <p class="text-xs text-on-surface-variant mt-1">{% translate 'Choose a date from the list below to revert to a previous version of this object.' %}</p>
27
+ </div>
28
+ </div>
29
+
30
+ <!-- Tab Switcher if connected inlines history is present -->
31
+ {% if orders_history or user_info_history %}
32
+ <div class="flex flex-wrap bg-surface-low border border-outline-variant/60 p-1.5 rounded-xl gap-2 w-max max-w-full shadow-sm mb-6">
33
+ <button type="button" class="tab-btn active bg-primary text-white text-[11px] font-bold px-4 py-2.5 rounded-lg shadow-sm transition-all focus:outline-none cursor-pointer" data-tab="model-history">
34
+ {{ opts.verbose_name|capfirst }} {% translate 'History' %}
35
+ </button>
36
+ {% if orders_history %}
37
+ <button type="button" class="tab-btn text-on-surface-variant hover:bg-surface-lowest/50 text-[11px] font-semibold px-4 py-2.5 rounded-lg transition-all focus:outline-none cursor-pointer" data-tab="related-orders-history">
38
+ {% translate 'Connected Orders History' %}
39
+ </button>
40
+ {% endif %}
41
+ {% if user_info_history %}
42
+ <button type="button" class="tab-btn text-on-surface-variant hover:bg-surface-lowest/50 text-[11px] font-semibold px-4 py-2.5 rounded-lg transition-all focus:outline-none cursor-pointer" data-tab="related-profile-history">
43
+ {% translate 'Connected Profile History' %}
44
+ </button>
45
+ {% endif %}
46
+ </div>
47
+ {% endif %}
48
+
49
+ <!-- Core Model History Content Tab -->
50
+ <div class="tab-content" id="model-history">
51
+ <div class="module">
52
+ {% if page_obj.object_list %}
53
+ {% include object_history_list_template %}
54
+ {% else %}
55
+ <p>{% trans "This object doesn't have a change history." %}</p>
56
+ {% endif %}
57
+ </div>
58
+ </div>
59
+
60
+ <!-- Related Orders History Content Tab -->
61
+ {% if orders_history %}
62
+ <div class="tab-content hidden" id="related-orders-history">
63
+ <table id="change-history">
64
+ <thead>
65
+ <tr>
66
+ <th scope="col">{% translate 'Order' %}</th>
67
+ <th scope="col">{% translate 'Product' %}</th>
68
+ <th scope="col">{% translate 'Date/time' %}</th>
69
+ <th scope="col">{% translate 'Comment' %}</th>
70
+ <th scope="col">{% translate 'Changed by' %}</th>
71
+ <th scope="col">{% translate 'Notes' %}</th>
72
+ <th scope="col">{% translate 'Changes' %}</th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ {% for record in orders_history %}
77
+ <tr>
78
+ <td>
79
+ <a href="{% url 'admin:crm_order_simple_history' record.id record.history_id %}">
80
+ Order #{{ record.id }}
81
+ </a>
82
+ </td>
83
+ <td>{{ record.product.name }}</td>
84
+ <td>{{ record.history_date }}</td>
85
+ <td>{{ record.get_history_type_display }}</td>
86
+ <td>{{ record.history_user|default:"None" }}</td>
87
+ <td>{{ record.notes|truncatechars:50 }}</td>
88
+ <td>
89
+ {% if record.history_delta_changes %}
90
+ <ul>
91
+ {% for change in record.history_delta_changes %}
92
+ <li>
93
+ <strong>{{ change.field }}:</strong> {{ change.old }} &rarr; {{ change.new }}
94
+ </li>
95
+ {% endfor %}
96
+ </ul>
97
+ {% else %}
98
+ <span class="text-xs text-on-surface-variant/60">No field changes</span>
99
+ {% endif %}
100
+ </td>
101
+ </tr>
102
+ {% endfor %}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+ {% endif %}
107
+
108
+ <!-- Related User Profile History Content Tab -->
109
+ {% if user_info_history %}
110
+ <div class="tab-content hidden" id="related-profile-history">
111
+ <table id="change-history">
112
+ <thead>
113
+ <tr>
114
+ <th scope="col">{% translate 'Profile' %}</th>
115
+ <th scope="col">{% translate 'Location' %}</th>
116
+ <th scope="col">{% translate 'Date/time' %}</th>
117
+ <th scope="col">{% translate 'Comment' %}</th>
118
+ <th scope="col">{% translate 'Changed by' %}</th>
119
+ <th scope="col">{% translate 'Website' %}</th>
120
+ <th scope="col">{% translate 'Changes' %}</th>
121
+ </tr>
122
+ </thead>
123
+ <tbody>
124
+ {% for record in user_info_history %}
125
+ <tr>
126
+ <td>
127
+ <a href="{% url 'admin:crm_userinformation_simple_history' record.id record.history_id %}">
128
+ Profile #{{ record.id }}
129
+ </a>
130
+ </td>
131
+ <td>{{ record.location|default:"None" }}</td>
132
+ <td>{{ record.history_date }}</td>
133
+ <td>{{ record.get_history_type_display }}</td>
134
+ <td>{{ record.history_user|default:"None" }}</td>
135
+ <td>{{ record.website|default:"None" }}</td>
136
+ <td>
137
+ {% if record.history_delta_changes %}
138
+ <ul>
139
+ {% for change in record.history_delta_changes %}
140
+ <li>
141
+ <strong>{{ change.field }}:</strong> {{ change.old }} &rarr; {{ change.new }}
142
+ </li>
143
+ {% endfor %}
144
+ </ul>
145
+ {% else %}
146
+ <span class="text-xs text-on-surface-variant/60">No field changes</span>
147
+ {% endif %}
148
+ </td>
149
+ </tr>
150
+ {% endfor %}
151
+ </tbody>
152
+ </table>
153
+ </div>
154
+ {% endif %}
155
+ </div>
156
+
157
+ <script>
158
+ document.addEventListener("DOMContentLoaded", function() {
159
+ const tabBtns = document.querySelectorAll('.tab-btn');
160
+ const tabContents = document.querySelectorAll('.tab-content');
161
+
162
+ tabBtns.forEach(btn => {
163
+ btn.addEventListener('click', function() {
164
+ const tabId = this.getAttribute('data-tab');
165
+
166
+ // Toggle buttons active state
167
+ tabBtns.forEach(b => {
168
+ b.classList.remove('active', 'bg-primary', 'text-white', 'shadow-sm', 'font-bold');
169
+ b.classList.add('text-on-surface-variant', 'hover:bg-surface-lowest/50', 'font-semibold');
170
+ });
171
+ this.classList.add('active', 'bg-primary', 'text-white', 'shadow-sm', 'font-bold');
172
+ this.classList.remove('text-on-surface-variant', 'hover:bg-surface-lowest/50', 'font-semibold');
173
+
174
+ // Toggle contents
175
+ tabContents.forEach(content => {
176
+ if (content.id === tabId) {
177
+ content.classList.remove('hidden');
178
+ } else {
179
+ content.classList.add('hidden');
180
+ }
181
+ });
182
+ });
183
+ });
184
+ });
185
+ </script>
186
+ {% endblock %}