Compare commits
174 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
3ce30cf5f7 | 3 days ago |
|
|
cf28db9fd0 | 3 days ago |
|
|
08fd01e119 | 6 days ago |
|
|
f97dbd79cc | 7 days ago |
|
|
240bb3fc25 | 7 days ago |
|
|
5102e4c295 | 7 days ago |
|
|
6be947706e | 1 week ago |
|
|
eb25d0e609 | 1 week ago |
|
|
efdb414345 | 2 weeks ago |
|
|
85c56981a6 | 2 weeks ago |
|
|
f01a681e93 | 2 weeks ago |
|
|
3522ee87f5 | 2 weeks ago |
|
|
e215ca7e1d | 2 weeks ago |
|
|
174c2988b2 | 2 weeks ago |
|
|
ec079e1a7a | 2 weeks ago |
|
|
a31796aad0 | 3 weeks ago |
|
|
7c31c511dd | 3 weeks ago |
|
|
441815d9a8 | 3 weeks ago |
|
|
521acaf747 | 3 weeks ago |
|
|
93a27f9583 | 3 weeks ago |
|
|
d9130b0fdf | 3 weeks ago |
|
|
1218c74d26 | 3 weeks ago |
|
|
49d497d48f | 3 weeks ago |
|
|
0d330f3dcf | 3 weeks ago |
|
|
34924db360 | 3 weeks ago |
|
|
11d6913807 | 3 weeks ago |
|
|
59a39ffd49 | 3 weeks ago |
|
|
7bf560a6a2 | 3 weeks ago |
|
|
77b999fbb3 | 3 weeks ago |
|
|
8de8a9ac49 | 4 weeks ago |
|
|
80b6bc1136 | 4 weeks ago |
|
|
1339b65731 | 4 weeks ago |
|
|
0158ce150d | 4 weeks ago |
|
|
fcb2ef9549 | 4 weeks ago |
|
|
005d8877e7 | 4 weeks ago |
|
|
093015dac6 | 4 weeks ago |
|
|
f152a441d4 | 4 weeks ago |
|
|
ef0e7b6326 | 4 weeks ago |
|
|
1572bed50d | 4 weeks ago |
|
|
4fb9460572 | 4 weeks ago |
|
|
015934c663 | 4 weeks ago |
|
|
16bc3e4428 | 4 weeks ago |
|
|
3db14c6180 | 4 weeks ago |
|
|
6918677009 | 4 weeks ago |
|
|
3b56d59321 | 4 weeks ago |
|
|
7c1c37746c | 4 weeks ago |
|
|
908c0b7dc8 | 1 month ago |
|
|
00228e4c8a | 1 month ago |
|
|
808d65a5a3 | 1 month ago |
|
|
0f7516a617 | 1 month ago |
|
|
1bf31744b5 | 1 month ago |
|
|
4df1ccba28 | 1 month ago |
|
|
108fd9cafa | 1 month ago |
|
|
76b0b02933 | 1 month ago |
|
|
6a8b5c4d97 | 1 month ago |
|
|
69ad1bef02 | 1 month ago |
|
|
632651a5ef | 1 month ago |
|
|
b6de2f5653 | 1 month ago |
|
|
8583523a0e | 1 month ago |
|
|
c6d5af345f | 1 month ago |
|
|
5c36eb8781 | 1 month ago |
|
|
35884b728f | 1 month ago |
|
|
511066ccc8 | 1 month ago |
|
|
c8c54b5ac8 | 1 month ago |
|
|
12ddbb2c29 | 1 month ago |
|
|
2d0dfd1b8f | 1 month ago |
|
|
ff7718d044 | 1 month ago |
|
|
169bc465c5 | 1 month ago |
|
|
7120bddd26 | 1 month ago |
|
|
58557c01aa | 1 month ago |
|
|
c37a6a8c12 | 1 month ago |
|
|
85cdf26fcf | 1 month ago |
|
|
ae4660ffb8 | 1 month ago |
|
|
358f025cc5 | 1 month ago |
|
|
95496508ac | 1 month ago |
|
|
a221bb0090 | 1 month ago |
|
|
a7dd1a4122 | 1 month ago |
|
|
23e1651dad | 1 month ago |
|
|
ff8788c527 | 1 month ago |
|
|
06e8375e15 | 1 month ago |
|
|
9e9d476922 | 1 month ago |
|
|
d9c7a1ae4a | 1 month ago |
|
|
73c18bfbf8 | 1 month ago |
|
|
5d60993748 | 1 month ago |
|
|
1404adc802 | 1 month ago |
|
|
f4d8b1a536 | 1 month ago |
|
|
22b06b4494 | 1 month ago |
|
|
c004325ac8 | 1 month ago |
|
|
a5c9765366 | 1 month ago |
|
|
4fbfce8393 | 1 month ago |
|
|
371bce35d7 | 1 month ago |
|
|
0074548dd4 | 1 month ago |
|
|
708e086ded | 1 month ago |
|
|
60d36278c6 | 1 month ago |
|
|
aa6cb5c84e | 1 month ago |
|
|
4e96ba5a13 | 1 month ago |
|
|
e5ac3750d1 | 1 month ago |
|
|
de5cd64679 | 1 month ago |
|
|
4826b4b8b7 | 1 month ago |
|
|
fe3e224d15 | 1 month ago |
|
|
8f7b21d0de | 1 month ago |
|
|
769f969ba2 | 1 month ago |
|
|
5e45b6d96a | 1 month ago |
|
|
1a9eb14dd9 | 1 month ago |
|
|
a8276d5f4b | 1 month ago |
|
|
c8ea7699c5 | 1 month ago |
|
|
e6756f40dd | 2 months ago |
|
|
9c2fbed0d5 | 2 months ago |
|
|
2c47025a77 | 2 months ago |
|
|
5754a655bc | 2 months ago |
|
|
de4336d13a | 2 months ago |
|
|
a7cbf4c6a6 | 2 months ago |
|
|
34d8fac0d5 | 2 months ago |
|
|
7d997fdb7d | 2 months ago |
|
|
40705b061c | 2 months ago |
|
|
1269b97765 | 2 months ago |
|
|
cf831be3c6 | 2 months ago |
|
|
e0047fbdc3 | 2 months ago |
|
|
9d71efb51a | 2 months ago |
|
|
03f860cf48 | 2 months ago |
|
|
409777f6b6 | 2 months ago |
|
|
8132826866 | 2 months ago |
|
|
33f170dd3e | 2 months ago |
|
|
9a3f92306d | 2 months ago |
|
|
6875081097 | 2 months ago |
|
|
ad6139852d | 2 months ago |
|
|
e6a6268143 | 2 months ago |
|
|
7709409a63 | 2 months ago |
|
|
1019c20890 | 2 months ago |
|
|
721650a8b6 | 2 months ago |
|
|
c0d97721dd | 2 months ago |
|
|
8c4799c1e6 | 2 months ago |
|
|
b1690b44c9 | 2 months ago |
|
|
05c70c94e1 | 2 months ago |
|
|
8c8cc21895 | 2 months ago |
|
|
c41eadfe36 | 2 months ago |
|
|
309e3d7ee1 | 2 months ago |
|
|
89e68c3033 | 2 months ago |
|
|
b3a20f69f4 | 2 months ago |
|
|
3af02f98a7 | 2 months ago |
|
|
e1e2fb08ef | 2 months ago |
|
|
146dae4039 | 2 months ago |
|
|
d4de2ae399 | 2 months ago |
|
|
f1c02a7d1b | 2 months ago |
|
|
147c8e9ba3 | 2 months ago |
|
|
09282698cf | 2 months ago |
|
|
26b7aea651 | 2 months ago |
|
|
289e8e8e8c | 2 months ago |
|
|
07d5d20800 | 2 months ago |
|
|
99a722c63c | 2 months ago |
|
|
7fec722362 | 2 months ago |
|
|
46001fc4e7 | 2 months ago |
|
|
17d5a1e7a5 | 2 months ago |
|
|
d5b2591925 | 2 months ago |
|
|
9717e71988 | 2 months ago |
|
|
ac76622995 | 2 months ago |
|
|
754c1d5796 | 2 months ago |
|
|
9666a998c4 | 2 months ago |
|
|
b2bd41b737 | 2 months ago |
|
|
980e5f6420 | 2 months ago |
|
|
75a00c0fa9 | 2 months ago |
|
|
319efc28f5 | 2 months ago |
|
|
334bcad30f | 2 months ago |
|
|
30b17810e9 | 2 months ago |
|
|
1482f7f670 | 2 months ago |
|
|
5320d0a5be | 2 months ago |
|
|
10168de3cd | 2 months ago |
|
|
c4be3c9ce2 | 2 months ago |
|
|
42bdb3bfed | 2 months ago |
|
|
bc792ed470 | 2 months ago |
|
|
6759ce7af8 | 2 months ago |
|
|
82900cfe5e | 2 months ago |
|
|
eee03f7708 | 2 months ago |
|
|
b71ac1c645 | 2 months ago |
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,38 @@ |
||||
# Generated by Django 5.1 on 2025-09-22 12:34 |
||||
|
||||
import django.db.models.deletion |
||||
import django.utils.timezone |
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('biz', '0004_prospect_contact_again'), |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='activity', |
||||
name='status', |
||||
field=models.CharField(blank=True, choices=[('NONE', 'None'), ('INBOUND', 'Inbound'), ('CONTACTED', 'Contacted'), ('RESPONDED', 'Responded'), ('SHOULD_TEST', 'Should test'), ('TESTING', 'Testing'), ('CUSTOMER', 'Customer'), ('LOST', 'Lost customer'), ('DECLINED', 'Declined'), ('NOT_CONCERNED', 'Not concerned'), ('SHOULD_BUY', 'Should buy'), ('HAVE_CREATED_ACCOUNT', 'Have created account')], max_length=50, null=True), |
||||
), |
||||
migrations.CreateModel( |
||||
name='Campaign', |
||||
fields=[ |
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)), |
||||
('last_update', models.DateTimeField(default=django.utils.timezone.now)), |
||||
('data_access_ids', models.JSONField(default=list)), |
||||
('name', models.CharField(blank=True, max_length=200, null=True)), |
||||
('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), |
||||
('prospects', models.ManyToManyField(blank=True, related_name='campaigns', to='biz.prospect')), |
||||
('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), |
||||
], |
||||
options={ |
||||
'abstract': False, |
||||
}, |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 5.1 on 2025-09-22 13:10 |
||||
|
||||
import uuid |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('biz', '0005_alter_activity_status_campaign'), |
||||
] |
||||
|
||||
operations = [ |
||||
# migrations.AlterField( |
||||
# model_name='campaign', |
||||
# name='id', |
||||
# field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False), |
||||
# ), |
||||
] |
||||
@ -0,0 +1,37 @@ |
||||
# Generated by Django 5.1 on 2025-09-22 14:08 |
||||
|
||||
import django.db.models.deletion |
||||
import django.utils.timezone |
||||
import uuid |
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('biz', '0006_alter_campaign_id'), |
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='ProspectGroup', |
||||
fields=[ |
||||
('creation_date', models.DateTimeField(default=django.utils.timezone.now, editable=False)), |
||||
('last_update', models.DateTimeField(default=django.utils.timezone.now)), |
||||
('data_access_ids', models.JSONField(default=list)), |
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)), |
||||
('name', models.CharField(blank=True, max_length=200, null=True)), |
||||
('last_updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), |
||||
('prospects', models.ManyToManyField(blank=True, related_name='prospect_groups', to='biz.prospect')), |
||||
('related_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), |
||||
], |
||||
options={ |
||||
'abstract': False, |
||||
}, |
||||
), |
||||
migrations.DeleteModel( |
||||
name='Campaign', |
||||
), |
||||
] |
||||
@ -0,0 +1,23 @@ |
||||
# Generated by Django 5.1 on 2025-10-15 07:46 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('biz', '0007_prospectgroup_delete_campaign'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AlterField( |
||||
model_name='activity', |
||||
name='declination_reason', |
||||
field=models.CharField(blank=True, choices=[('TOO_EXPENSIVE', 'Too expensive'), ('USE_OTHER_PRODUCT', 'Use other product'), ('USE_ANDROID', 'Use Android'), ('TOO_FEW_TOURNAMENTS', 'Too few tournaments'), ('NOT_INTERESTED', 'Not interested'), ('UNKNOWN', 'Unknown')], max_length=50, null=True), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='activity', |
||||
name='type', |
||||
field=models.CharField(blank=True, choices=[('MAIL', 'Mail'), ('SMS', 'SMS'), ('CALL', 'Call'), ('PRESS', 'Press Release'), ('WORD_OF_MOUTH', 'Word of mouth'), ('WHATS_APP', 'WhatsApp')], max_length=20, null=True), |
||||
), |
||||
] |
||||
@ -0,0 +1,448 @@ |
||||
{% extends "admin/base_site.html" %} |
||||
{% load static %} |
||||
|
||||
{% block extrahead %} |
||||
{{ block.super }} |
||||
<style> |
||||
.dashboard-container { |
||||
padding: 20px; |
||||
} |
||||
|
||||
.filter-switch { |
||||
margin-bottom: 20px; |
||||
padding: 15px; |
||||
background: #f8f8f8; |
||||
border: 1px solid #ddd; |
||||
border-radius: 4px; |
||||
} |
||||
|
||||
.filter-switch label { |
||||
font-weight: bold; |
||||
margin-right: 10px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.filter-switch input[type="checkbox"] { |
||||
cursor: pointer; |
||||
width: 18px; |
||||
height: 18px; |
||||
vertical-align: middle; |
||||
} |
||||
|
||||
.status-section { |
||||
margin-bottom: 30px; |
||||
background: white; |
||||
border: 1px solid #ddd; |
||||
border-radius: 4px; |
||||
overflow: hidden; |
||||
} |
||||
|
||||
.status-header { |
||||
background: #417690; |
||||
color: white; |
||||
padding: 12px 15px; |
||||
font-weight: bold; |
||||
font-size: 14px; |
||||
} |
||||
|
||||
.status-header .status-name { |
||||
font-size: 16px; |
||||
margin-right: 10px; |
||||
} |
||||
|
||||
.status-header .count { |
||||
font-size: 13px; |
||||
opacity: 0.9; |
||||
} |
||||
|
||||
.prospect-table { |
||||
width: 100%; |
||||
border-collapse: collapse; |
||||
table-layout: fixed; |
||||
} |
||||
|
||||
.prospect-table thead { |
||||
background: #f9f9f9; |
||||
border-bottom: 2px solid #ddd; |
||||
} |
||||
|
||||
.prospect-table thead th { |
||||
padding: 10px 12px; |
||||
text-align: left; |
||||
font-weight: 600; |
||||
font-size: 13px; |
||||
color: #666; |
||||
border-bottom: 1px solid #ddd; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(1), |
||||
.prospect-table td:nth-child(1) { |
||||
width: 225px; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(2), |
||||
.prospect-table td:nth-child(2) { |
||||
width: auto; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(3), |
||||
.prospect-table td:nth-child(3) { |
||||
width: 120px; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(4), |
||||
.prospect-table td:nth-child(4) { |
||||
width: 140px; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(5), |
||||
.prospect-table td:nth-child(5) { |
||||
width: 130px; |
||||
} |
||||
|
||||
.prospect-table th:nth-child(6), |
||||
.prospect-table td:nth-child(6) { |
||||
width: 130px; |
||||
} |
||||
|
||||
.prospect-table th.actions-col, |
||||
.prospect-table td.actions-col { |
||||
width: 80px; |
||||
text-align: center; |
||||
} |
||||
|
||||
.add-activity-btn { |
||||
display: inline-flex; |
||||
align-items: center; |
||||
justify-content: center; |
||||
width: 24px; |
||||
height: 24px; |
||||
background: #70bf2b; |
||||
color: white !important; |
||||
text-decoration: none !important; |
||||
border-radius: 50%; |
||||
font-size: 18px; |
||||
font-weight: bold; |
||||
transition: background-color 0.2s; |
||||
} |
||||
|
||||
.add-activity-btn:hover { |
||||
background: #5fa624; |
||||
color: white !important; |
||||
text-decoration: none !important; |
||||
} |
||||
|
||||
.prospect-table tbody tr { |
||||
border-bottom: 1px solid #eee; |
||||
transition: background-color 0.2s; |
||||
} |
||||
|
||||
.prospect-table tbody tr:hover { |
||||
background: #f5f5f5; |
||||
} |
||||
|
||||
.prospect-table tbody td { |
||||
padding: 10px 12px; |
||||
font-size: 13px; |
||||
} |
||||
|
||||
.prospect-table tbody td a { |
||||
color: #417690; |
||||
text-decoration: none; |
||||
} |
||||
|
||||
.prospect-table tbody td a:hover { |
||||
text-decoration: underline; |
||||
} |
||||
|
||||
.prospect-name { |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.prospect-entity { |
||||
color: #666; |
||||
font-style: italic; |
||||
overflow: hidden; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
.prospect-date { |
||||
color: #666; |
||||
white-space: nowrap; |
||||
} |
||||
|
||||
.prospect-status { |
||||
display: inline-block; |
||||
padding: 3px 8px; |
||||
background: #e8f4f8; |
||||
border-radius: 3px; |
||||
font-size: 11px; |
||||
color: #417690; |
||||
font-weight: 500; |
||||
} |
||||
|
||||
.empty-state { |
||||
padding: 40px 15px; |
||||
text-align: center; |
||||
color: #999; |
||||
font-style: italic; |
||||
} |
||||
</style> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<div class="dashboard-container"> |
||||
|
||||
<!-- Quick Actions --> |
||||
<div class="quick-actions" style="background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 12px; padding: 25px; margin-bottom: 20px;"> |
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 15px;"> |
||||
<a href="{% url 'admin:biz_prospect_changelist' %}" |
||||
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> |
||||
Prospects |
||||
</a> |
||||
<a href="{% url 'admin:biz_activity_changelist' %}" |
||||
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> |
||||
Activities |
||||
</a> |
||||
<a href="{% url 'admin:biz_entity_changelist' %}" |
||||
style="display: block; padding: 12px 15px; background: #17a2b8; color: white; text-decoration: none; border-radius: 8px; text-align: center; font-weight: 500;"> |
||||
Entities |
||||
</a> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="filter-switch"> |
||||
<label for="my-prospects-toggle"> |
||||
<input type="checkbox" id="my-prospects-toggle" {% if filter_my %}checked{% endif %}> |
||||
Show only my prospects |
||||
</label> |
||||
</div> |
||||
|
||||
<!-- CONTACT AGAIN Section --> |
||||
<div class="status-section"> |
||||
<div class="status-header"> |
||||
<span class="status-name">CONTACT AGAIN</span> |
||||
<span class="count">({{ contact_again_prospects.count }})</span> |
||||
</div> |
||||
{% if contact_again_prospects %} |
||||
<table class="prospect-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Entity</th> |
||||
<th>Phone</th> |
||||
<th>Status</th> |
||||
<th>Contact Again</th> |
||||
<th>Last Update</th> |
||||
<th class="actions-col">Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in contact_again_prospects %} |
||||
<tr> |
||||
<td> |
||||
<a href="{% url 'admin:biz_prospect_change' prospect.id %}" class="prospect-name"> |
||||
{{ prospect.first_name|default:"" }} {{ prospect.last_name|default:"" }} |
||||
</a> |
||||
</td> |
||||
<td class="prospect-entity">{{ prospect.entity_names }}</td> |
||||
<td>{{ prospect.phone|default:"-" }}</td> |
||||
<td><span class="prospect-status">{{ prospect.current_status }}</span></td> |
||||
<td class="prospect-date">{{ prospect.contact_again|date:"d/m/Y" }}</td> |
||||
<td class="prospect-date">{{ prospect.last_update|date:"d/m/Y H:i" }}</td> |
||||
<td class="actions-col"> |
||||
<a href="{% url 'admin:biz_activity_add' %}?prospect={{ prospect.id }}" class="add-activity-btn" title="Add Activity">+</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
{% else %} |
||||
<div class="empty-state">No prospects</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<!-- SHOULD_TEST Section --> |
||||
<div class="status-section"> |
||||
<div class="status-header"> |
||||
<span class="status-name">SHOULD TEST</span> |
||||
<span class="count">({{ should_test_prospects.count }})</span> |
||||
</div> |
||||
{% if should_test_prospects %} |
||||
<table class="prospect-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Entity</th> |
||||
<th>Phone</th> |
||||
<th>Activity Type</th> |
||||
<th>Last Update</th> |
||||
<th class="actions-col">Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in should_test_prospects %} |
||||
<tr> |
||||
<td> |
||||
<a href="{% url 'admin:biz_prospect_change' prospect.id %}" class="prospect-name"> |
||||
{{ prospect.first_name|default:"" }} {{ prospect.last_name|default:"" }} |
||||
</a> |
||||
</td> |
||||
<td class="prospect-entity">{{ prospect.entity_names }}</td> |
||||
<td>{{ prospect.phone|default:"-" }}</td> |
||||
<td>{{ prospect.current_activity_type|default:"-" }}</td> |
||||
<td class="prospect-date">{{ prospect.last_update|date:"d/m/Y H:i" }}</td> |
||||
<td class="actions-col"> |
||||
<a href="{% url 'admin:biz_activity_add' %}?prospect={{ prospect.id }}" class="add-activity-btn" title="Add Activity">+</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
{% else %} |
||||
<div class="empty-state">No prospects</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<!-- TESTING Section --> |
||||
<div class="status-section"> |
||||
<div class="status-header"> |
||||
<span class="status-name">TESTING</span> |
||||
<span class="count">({{ testing_prospects.count }})</span> |
||||
</div> |
||||
{% if testing_prospects %} |
||||
<table class="prospect-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Entity</th> |
||||
<th>Phone</th> |
||||
<th>Activity Type</th> |
||||
<th>Last Update</th> |
||||
<th class="actions-col">Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in testing_prospects %} |
||||
<tr> |
||||
<td> |
||||
<a href="{% url 'admin:biz_prospect_change' prospect.id %}" class="prospect-name"> |
||||
{{ prospect.first_name|default:"" }} {{ prospect.last_name|default:"" }} |
||||
</a> |
||||
</td> |
||||
<td class="prospect-entity">{{ prospect.entity_names }}</td> |
||||
<td>{{ prospect.phone|default:"-" }}</td> |
||||
<td>{{ prospect.current_activity_type|default:"-" }}</td> |
||||
<td class="prospect-date">{{ prospect.last_update|date:"d/m/Y H:i" }}</td> |
||||
<td class="actions-col"> |
||||
<a href="{% url 'admin:biz_activity_add' %}?prospect={{ prospect.id }}" class="add-activity-btn" title="Add Activity">+</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
{% else %} |
||||
<div class="empty-state">No prospects</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<!-- OTHERS Section --> |
||||
<div class="status-section"> |
||||
<div class="status-header"> |
||||
<span class="status-name">OTHERS</span> |
||||
<span class="count">({{ others_prospects.count }})</span> |
||||
</div> |
||||
{% if others_prospects %} |
||||
<table class="prospect-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Entity</th> |
||||
<th>Phone</th> |
||||
<th>Status</th> |
||||
<th>Activity Type</th> |
||||
<th>Last Update</th> |
||||
<th class="actions-col">Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in others_prospects %} |
||||
<tr> |
||||
<td> |
||||
<a href="{% url 'admin:biz_prospect_change' prospect.id %}" class="prospect-name"> |
||||
{{ prospect.first_name|default:"" }} {{ prospect.last_name|default:"" }} |
||||
</a> |
||||
</td> |
||||
<td class="prospect-entity">{{ prospect.entity_names }}</td> |
||||
<td>{{ prospect.phone|default:"-" }}</td> |
||||
<td><span class="prospect-status">{{ prospect.current_status }}</span></td> |
||||
<td>{{ prospect.current_activity_type|default:"-" }}</td> |
||||
<td class="prospect-date">{{ prospect.last_update|date:"d/m/Y H:i" }}</td> |
||||
<td class="actions-col"> |
||||
<a href="{% url 'admin:biz_activity_add' %}?prospect={{ prospect.id }}" class="add-activity-btn" title="Add Activity">+</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
{% else %} |
||||
<div class="empty-state">No prospects</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
<!-- RESPONDED Section --> |
||||
<div class="status-section"> |
||||
<div class="status-header"> |
||||
<span class="status-name">RESPONDED</span> |
||||
<span class="count">({{ responded_prospects.count }})</span> |
||||
</div> |
||||
{% if responded_prospects %} |
||||
<table class="prospect-table"> |
||||
<thead> |
||||
<tr> |
||||
<th>Name</th> |
||||
<th>Entity</th> |
||||
<th>Phone</th> |
||||
<th>Activity Type</th> |
||||
<th>Last Update</th> |
||||
<th class="actions-col">Actions</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{% for prospect in responded_prospects %} |
||||
<tr> |
||||
<td> |
||||
<a href="{% url 'admin:biz_prospect_change' prospect.id %}" class="prospect-name"> |
||||
{{ prospect.first_name|default:"" }} {{ prospect.last_name|default:"" }} |
||||
</a> |
||||
</td> |
||||
<td class="prospect-entity">{{ prospect.entity_names }}</td> |
||||
<td>{{ prospect.phone|default:"-" }}</td> |
||||
<td>{{ prospect.current_activity_type|default:"-" }}</td> |
||||
<td class="prospect-date">{{ prospect.last_update|date:"d/m/Y H:i" }}</td> |
||||
<td class="actions-col"> |
||||
<a href="{% url 'admin:biz_activity_add' %}?prospect={{ prospect.id }}" class="add-activity-btn" title="Add Activity">+</a> |
||||
</td> |
||||
</tr> |
||||
{% endfor %} |
||||
</tbody> |
||||
</table> |
||||
{% else %} |
||||
<div class="empty-state">No prospects</div> |
||||
{% endif %} |
||||
</div> |
||||
|
||||
</div> |
||||
|
||||
<script> |
||||
document.getElementById('my-prospects-toggle').addEventListener('change', function(e) { |
||||
const url = new URL(window.location); |
||||
if (e.target.checked) { |
||||
url.searchParams.set('my', 'true'); |
||||
} else { |
||||
url.searchParams.delete('my'); |
||||
} |
||||
window.location.href = url.toString(); |
||||
}); |
||||
</script> |
||||
{% endblock %} |
||||
@ -1,57 +1,54 @@ |
||||
|
||||
|
||||
# Rest Framework configuration |
||||
REST_FRAMEWORK = { |
||||
'DATETIME_FORMAT': "%Y-%m-%dT%H:%M:%S.%f%z", |
||||
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%S.%f%z", |
||||
# Use Django's standard `django.contrib.auth` permissions, |
||||
# or allow read-only access for unauthenticated users. |
||||
'DEFAULT_PERMISSION_CLASSES': [ |
||||
'rest_framework.permissions.IsAuthenticated', |
||||
"DEFAULT_PERMISSION_CLASSES": [ |
||||
"rest_framework.permissions.IsAuthenticated", |
||||
], |
||||
"DEFAULT_AUTHENTICATION_CLASSES": [ |
||||
"rest_framework.authentication.BasicAuthentication", |
||||
"rest_framework.authentication.TokenAuthentication", |
||||
"rest_framework.authentication.SessionAuthentication", |
||||
], |
||||
'DEFAULT_AUTHENTICATION_CLASSES': [ |
||||
'rest_framework.authentication.BasicAuthentication', |
||||
'rest_framework.authentication.TokenAuthentication', |
||||
'rest_framework.authentication.SessionAuthentication', |
||||
] |
||||
} |
||||
|
||||
EMAIL_HOST_USER = 'automatic@padelclub.app' |
||||
EMAIL_HOST_PASSWORD = 'XLR@Sport@2024' |
||||
DEFAULT_FROM_EMAIL = 'Padel Club <automatic@padelclub.app>' |
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' |
||||
EMAIL_HOST = 'smtp-xlr.alwaysdata.net' |
||||
EMAIL_HOST_USER = "automatic@padelclub.app" |
||||
EMAIL_HOST_PASSWORD = "XLR@Sport@2024" |
||||
DEFAULT_FROM_EMAIL = "Padel Club <automatic@padelclub.app>" |
||||
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" |
||||
EMAIL_HOST = "smtp-xlr.alwaysdata.net" |
||||
EMAIL_PORT = 587 |
||||
EMAIL_USE_TLS = True |
||||
|
||||
CACHES = { |
||||
'default': { |
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', |
||||
"default": { |
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", |
||||
}, |
||||
"qr-code": { |
||||
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", |
||||
"LOCATION": "qr-code-cache", |
||||
"TIMEOUT": 3600, |
||||
}, |
||||
'qr-code': { |
||||
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', |
||||
'LOCATION': 'qr-code-cache', |
||||
'TIMEOUT': 3600 |
||||
} |
||||
} |
||||
|
||||
QR_CODE_CACHE_ALIAS = 'qr-code' |
||||
QR_CODE_CACHE_ALIAS = "qr-code" |
||||
|
||||
SYNC_APPS = { |
||||
'sync': {}, |
||||
'tournaments': { 'exclude': ['Log', 'FailedApiCall', 'DeviceToken', 'Image'] }, |
||||
'biz': {}, |
||||
"sync": {}, |
||||
"tournaments": {"exclude": ["Log", "FailedApiCall", "DeviceToken", "Image"]}, |
||||
# 'biz': {}, |
||||
} |
||||
|
||||
SYNC_MODEL_CHILDREN_SHARING = { |
||||
'Match': ['team_scores', 'team_registration', 'player_registrations'] |
||||
"Match": ["team_scores", "team_registration", "player_registrations"] |
||||
} |
||||
|
||||
STRIPE_CURRENCY = 'eur' |
||||
STRIPE_CURRENCY = "eur" |
||||
# Add managers who should receive internal emails |
||||
SHOP_MANAGERS = [ |
||||
('Shop Admin', 'shop-admin@padelclub.app'), |
||||
("Shop Admin", "shop-admin@padelclub.app"), |
||||
# ('Laurent Morvillier', 'laurent@padelclub.app'), |
||||
# ('Xavier Rousset', 'xavier@padelclub.app'), |
||||
] |
||||
SHOP_SITE_ROOT_URL = 'https://padelclub.app' |
||||
SHOP_SUPPORT_EMAIL = 'shop@padelclub.app' |
||||
SHOP_SITE_ROOT_URL = "https://padelclub.app" |
||||
SHOP_SUPPORT_EMAIL = "shop@padelclub.app" |
||||
|
||||
@ -0,0 +1,21 @@ |
||||
### Synchronization quick ReadMe |
||||
|
||||
- Data class must extend BaseModel |
||||
- Admin classes must extend SyncedObjectAdmin to have updates saved in the BaseModel properties |
||||
- The SynchronizationApi defines a get and a post service to POST new data, and GET updates. When performing an operation on a data, a ModelLog instance is created with the related information. When performing a GET, we retrieve the list of ModelLogs to sent the new data to the user. |
||||
- routing.py defines the URL of the websocket where messages are sent when updates are made. URL is by user. |
||||
|
||||
|
||||
### Sharing |
||||
|
||||
- Data can be shared between users. To do that, a new DataAccess object can be created to define the owner, the authorized user, and the object id. |
||||
- By default, the whole hierarchy of objects are shared, from the data parents to all its children. |
||||
- Special data path can be specified for a class by defining a setting |
||||
|
||||
example: |
||||
SYNC_MODEL_CHILDREN_SHARING = { |
||||
'Match': ['team_scores', 'team_registration', 'player_registrations'] |
||||
} |
||||
Here when sharing a Match, we also share objects accessed through the names of the properties to get TeamScore, TeamRegistration and PlayerRegistration. |
||||
|
||||
- It's also possible to exclude a class from being sharable by setting sharable = False in its definition. In PadelClub, Club is the top entity that links all data together, so we don't want the automatic data scanning to share clubs. |
||||
File diff suppressed because it is too large
Load Diff
@ -1,53 +0,0 @@ |
||||
from django.urls import reverse |
||||
from django.utils import timezone |
||||
import datetime |
||||
|
||||
class ReferrerMiddleware: |
||||
def __init__(self, get_response): |
||||
self.get_response = get_response |
||||
|
||||
def __call__(self, request): |
||||
# Check if the user is anonymous and going to the login page |
||||
if not request.user.is_authenticated and request.path == reverse('login'): |
||||
# Get the referring URL from the HTTP_REFERER header |
||||
referrer = request.META.get('HTTP_REFERER') |
||||
|
||||
# Only store referrer if it exists and is not the login page itself |
||||
if referrer and 'login' not in referrer: |
||||
request.session['login_referrer'] = referrer |
||||
|
||||
response = self.get_response(request) |
||||
return response |
||||
|
||||
class RegistrationCartCleanupMiddleware: |
||||
def __init__(self, get_response): |
||||
self.get_response = get_response |
||||
|
||||
def __call__(self, request): |
||||
self._check_and_clean_expired_cart(request) |
||||
response = self.get_response(request) |
||||
return response |
||||
|
||||
def _check_and_clean_expired_cart(self, request): |
||||
if 'registration_cart_expiry' in request.session: |
||||
try: |
||||
expiry_str = request.session['registration_cart_expiry'] |
||||
expiry = datetime.datetime.fromisoformat(expiry_str) |
||||
if timezone.now() > expiry: |
||||
# Clear expired cart |
||||
keys_to_delete = [ |
||||
'registration_cart_id', |
||||
'registration_tournament_id', |
||||
'registration_cart_players', |
||||
'registration_cart_expiry', |
||||
'registration_mobile_number' |
||||
] |
||||
for key in keys_to_delete: |
||||
if key in request.session: |
||||
del request.session[key] |
||||
request.session.modified = True |
||||
except (ValueError, TypeError): |
||||
# Invalid expiry format, clear it |
||||
if 'registration_cart_expiry' in request.session: |
||||
del request.session['registration_cart_expiry'] |
||||
request.session.modified = True |
||||
@ -0,0 +1,28 @@ |
||||
# Generated by Django 5.1 on 2025-09-24 14:19 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tournaments', '0137_playerregistration_is_anonymous_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.RemoveField( |
||||
model_name='customuser', |
||||
name='agents', |
||||
), |
||||
migrations.AddField( |
||||
model_name='customuser', |
||||
name='supervisors', |
||||
field=models.ManyToManyField(blank=True, related_name='supervising_for', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
migrations.AlterField( |
||||
model_name='tournament', |
||||
name='animation_type', |
||||
field=models.IntegerField(choices=[(0, 'Tournoi'), (1, 'Mêlée'), (2, 'Classement'), (3, 'Consolation'), (4, 'Custom')], default=0), |
||||
), |
||||
] |
||||
@ -0,0 +1,19 @@ |
||||
# Generated by Django 5.1 on 2025-09-24 14:20 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tournaments', '0138_remove_customuser_agents_customuser_supervisors_and_more'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='customuser', |
||||
name='organizers', |
||||
field=models.ManyToManyField(blank=True, related_name='organising_for', to=settings.AUTH_USER_MODEL), |
||||
), |
||||
] |
||||
@ -0,0 +1,18 @@ |
||||
# Generated by Django 5.1 on 2025-10-15 08:02 |
||||
|
||||
from django.db import migrations, models |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
|
||||
dependencies = [ |
||||
('tournaments', '0139_customuser_organizers'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.AddField( |
||||
model_name='tournament', |
||||
name='custom_club_name', |
||||
field=models.CharField(blank=True, max_length=100, null=True), |
||||
), |
||||
] |
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,81 @@ |
||||
{% extends "admin/base_site.html" %} |
||||
{% load static %} |
||||
|
||||
{% block extrahead %} |
||||
{{ block.super }} |
||||
{{ media }} |
||||
<script type="text/javascript" src="{% url 'admin:jsi18n' %}"></script> |
||||
{% endblock %} |
||||
|
||||
{% block extrastyle %} |
||||
{{ block.super }} |
||||
<link rel="stylesheet" type="text/css" href="{% static 'admin/css/forms.css' %}"> |
||||
{% endblock %} |
||||
|
||||
{% block breadcrumbs %} |
||||
<div class="breadcrumbs"> |
||||
<a href="{% url 'admin:index' %}">Home</a> |
||||
› <a href="{% url 'admin:tournaments_event_changelist' %}">Events</a> |
||||
› Set Club |
||||
</div> |
||||
{% endblock %} |
||||
|
||||
{% block content %} |
||||
<h1>{{ title }}</h1> |
||||
|
||||
<p>You are about to set the club for the following {{ events|length }} event(s):</p> |
||||
|
||||
<div style="margin: 20px 0; padding: 15px; background-color: #f8f9fa; border: 1px solid #dee2e6; border-radius: 4px;"> |
||||
<ul style="margin: 0; padding-left: 20px;"> |
||||
{% for event in events %} |
||||
<li> |
||||
<strong>{{ event.name }}</strong> |
||||
{% if event.club %} |
||||
(currently: {{ event.club.name }}) |
||||
{% else %} |
||||
(currently: No club assigned) |
||||
{% endif %} |
||||
</li> |
||||
{% endfor %} |
||||
</ul> |
||||
</div> |
||||
|
||||
<form method="post" id="club-form" action=""> |
||||
{% csrf_token %} |
||||
|
||||
{# Hidden fields to preserve the selection #} |
||||
{% for hidden in form.hidden_fields %} |
||||
{{ hidden }} |
||||
{% endfor %} |
||||
|
||||
{% if form.non_field_errors %} |
||||
<div class="errors"> |
||||
{{ form.non_field_errors }} |
||||
</div> |
||||
{% endif %} |
||||
|
||||
<div style="margin: 20px 0;"> |
||||
<fieldset class="module aligned"> |
||||
<div class="form-row field-club_id"> |
||||
<div> |
||||
<label for="{{ form.club_id.id_for_label }}" class="required">{{ form.club_id.label }}:</label> |
||||
<div class="related-widget-wrapper"> |
||||
{{ form.club_id }} |
||||
</div> |
||||
{% if form.club_id.help_text %} |
||||
<div class="help">{{ form.club_id.help_text }}</div> |
||||
{% endif %} |
||||
{% if form.club_id.errors %} |
||||
<div class="errors">{{ form.club_id.errors }}</div> |
||||
{% endif %} |
||||
</div> |
||||
</div> |
||||
</fieldset> |
||||
</div> |
||||
|
||||
<div class="submit-row" style="margin-top: 20px; padding: 10px; text-align: right;"> |
||||
<input type="submit" name="apply" value="Set Club" class="default" style="margin-right: 10px;"/> |
||||
<a href="{% url 'admin:tournaments_event_changelist' %}" class="button cancel-link">Cancel</a> |
||||
</div> |
||||
</form> |
||||
{% endblock %} |
||||
@ -0,0 +1,30 @@ |
||||
{% extends 'tournaments/base.html' %} |
||||
{% block head_title %} Paiement {% endblock %} |
||||
{% block first_title %} Padel Club {% endblock %} |
||||
{% block second_title %} Paiement {% endblock %} |
||||
|
||||
{% block content %} |
||||
{% load static %} |
||||
{% load tz %} |
||||
|
||||
<div class="grid-x"> |
||||
<div class="bubble"> |
||||
<div class="cell medium-6 large-6 padding10"> |
||||
{% if payment_status == 'success' %} |
||||
<label class="title">Paiement réussi !</label> |
||||
<p>Votre inscription a été confirmée. Un email de confirmation vous a été envoyé.</p> |
||||
{% if show_details and tournament %} |
||||
<p>Tournoi : <strong>{{ tournament.display_name }}</strong></p> |
||||
{% endif %} |
||||
{% elif payment_status == 'cancel' %} |
||||
<label class="title">Paiement annulé</label> |
||||
<p>Votre paiement a été annulé. Aucun montant n'a été prélevé.</p> |
||||
{% else %} |
||||
<label class="title">Paiement en cours de traitement</label> |
||||
<p>Votre paiement est en cours de traitement. Vous recevrez un email de confirmation sous peu.</p> |
||||
{% endif %} |
||||
<p>Vous pouvez maintenant fermer cette page.</p> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
{% endblock %} |
||||
Loading…
Reference in new issue