From 0fbf30ddc558c4e0684fa492ff7fa67037077327 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Feb 2026 20:41:13 +0000 Subject: [PATCH] Ver 11.01 better permisions --- core/__pycache__/views.cpython-311.pyc | Bin 38557 -> 39478 bytes core/templates/base.html | 33 +++++++- core/templates/core/log_attendance.html | 99 ++++++++++++++++++++++-- core/templates/core/work_log_list.html | 23 ++++-- core/views.py | 16 +++- 5 files changed, 153 insertions(+), 18 deletions(-) diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2ec507423d94549587d3c24b62081c9d4240c880..499a72a35690b53061bca428ae48605618c937ce 100644 GIT binary patch delta 6343 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 delta 5384 zcmbtYdvKK172mtBY@P%{lHEKvZ$3i;7$X5BfPy3_7$BlhVe_&3CD~*j@qQaHannGH zsBOjIwT@L8?Q}+sk5;;^+D>2Eck8oSvEQ_`+K25womlHkYscF2J6|41Yo{~aneUhL zoqNA??>YB(&bjd9SDdFFcNRR9pP%EP-*?p~qsNb)F4*ACanh@co_CI`YurtFLwM)k z11@#4sJ3v#pLPtnKdf}`c4$sj=-E)Fc`RSl44Emtzu(G(G&*9)QPYs>xaUYt3w>8g zkGWYDdaENCOXvFyw2iFp|}jN8L$PgRjtZjA-a^FUlm*f z#ZCgt6-y3S1*yZyP&}DP4Voc+ASt`m{rUBU%Q5aAOvVkVlUSK~C4YxA*h*qp5X}9u zNnoGvq-fk+o!M0QF(G=?sl_2toq2!p^G@-D%ukoD7UBX`Q@(6-x-cRl75BL(N>a^p zCLE5mBT}3$d64Wp3S0c!?TA!Pl+xp;Cy*}K?3fb!AD&p8TB}|z*(<74b7`9>Q=!sI zCEEgOV^%?-d@Y?HsUGIA%k}9p^_yEt)P}`N>pxX}OLf191dsXBIh!31lKn?#S8+Hd z%G3UI*#p%|_?rU$gd!6)vnFm!mu+_JHzz7mD{0LZt>UrT2g&%O^XEEInXaT6+b4?B zMMK^>lMe*d^~;Mzg?ijyDXQt@>+&mSEiO$LJy4_G-c_%v10GLcZr!Xvx?CRQRf^SJ z{u1?xK&z-zrvsJ1wvx#QDddmNhg~?*rK#?@q89=ZT2ngNLPJ00>Y8nzA!a% zfx4}1pQkxM=lov|S!>?WE8Enm@_O$DWJvB&KP+GA-3@WSdcVA)@In&*gxJcZ zfOSf%=wG>qL@W1jQVtu^427Zz*+H6dXK>lLuVHUXL%gLS@}AH%w~XVXgqp5wP;XTB zFPU~s3mGAu=CYivD=l$Yg{sQLay4GnCZ^RFt5&pv;#PL@kRcDtXv)x>601pUiR`hw zABrJBSmjplDn){5O%mxTw*e3nO3A&!@@9NzZ`?=?CL^*6 z^ZV8P!4;xcy%=n(-US7GvV1hRKNY1kEQW3w;0o1NbCD}3uT?`ejiq~`f}d6n6%UyT zMf8**2h>9~D{~iiI;`HTX)iy3x#O5y5KbogW6^LbWF%5@O!lcOYOfai)aPr9vTvjb zW{^NVQ(Nf|1~*Dn1G#bz;FGyV{k8Ut7|c9ZcaIRoYM|kambJJ?{3clctCm%d?R%-C z>*bd2S6jMYU(xY;Q}ax=r@CauK`^!T-Mq}*jl)jgPS>Ewu79^z3>80}aCl0@yyW!Q`4KKMj$kxoA+D~!|c_;g@9R=90676N$5r`b_ zf_Y}73_WhjFpXGQBYHFfs;p9{+XKZnkZxL7**t?N6d__H)vw#vigs0Y@kF2p<4!pS zqX?LZtPcj|ay5PNN>Qn-i(9xF$~MfvQE53=UMiW=VpQCJTVM>8QKC;+uB}^oPSb)@RfLH znt>LUr#~u*D7QlE0bB*(w7D9Ry@@Do>T7uYI)Rn52c+C0Wm3wgS;0*}>WKlv@_|H5 zyB;Oh1ni9LRlzmOC!d3b8hqd#9bvA$3=Jx`Rwb{+K(>7V6jdt=0VYA7#w65U`4a&8 zkNhzK1r0N1k4)lJt(^X7g5pTs$&}AR;YlSag7PJj>*R~jh>+z=jK+!VW2U^5G;#pI z1n*OSUK12As6}gw#l5OwZFeW%L5NhF57m$Q&K;oL(>N-Pcx7bwt8a_!;N z^Wheg(BpcmNZ=C9kFF7p2Dp?D^;<3&-k65oCo&;h`XH5dPp-{T?G+f%D?)-~Y0 z5#R3Ac%%JffVRg(Gu?x_xiVy4<{!b)WNSOsXaOLG}VNUFT1unT) zKoKO%OEz`*s~nFqhUq;zhYohVa@AXvX9|l1x7V^?iSx zP2j~NRCklH7!_J`@L*Dx5!pd=e?*Ttaz|o?DjUzsg!|S|#$`^0>#{cT89xb|V6to{ z=#^zKUje8D)Bteub42Ihu)B4446hRbb;qh;Pd;&*d_aj5j zM}xS_7OXNKb{T+XIGM4 z>U8X2Jc2LXCh=J=g5fE=XK%S7Y>cF$$%H(PcNcYMt>~QU$N7CBcGw134`>H80#*Z_ zg|$mbva-z4gF{9*MYpsg@jkk)z7a3>?52^5%s=AKdhGiU$DCu>qMkSJ=tPg98!O7) zIr#x@l6H*#SR%$(8@Y}wnJ9QvwvO0$y=T;^RHL|6eJ{0h32wxihiBQ`38<#giv&)j za+1%18O-UxtmnqSfp_6kpY8TjB+q^i@2&w{3+TfD@4-hB?Eydg&dDcY#-bykh>_Bx zF}asM7$@eKZ_(7h9dW3?jsBjPclJ<+a~U&_tBY3;r=ncgs3j)l*yVPCSw-LU$_~{z zRv~t)kBya2{`Y(@!^W5~3;E6rWVhV5TL<96{y9J9!Z=(0v%wwvx)Gu&pPk$J5yy+6@=8NDiAH>~IOA!zUFjBbMpvgMswWGu2dttk z`RI1Q#<>Zc$A9h#?A(IaoG0^~KDRO09h`TdV_DiPe}a7 z!E{Fd_(g*Jy=(Gj_onmwoljURH)5D!8MPA#o?BTQNxDZO1UqCM*}^{@4;*W2Mum_; zz(E3PT^zi5W75D7jL6H>H;(x^P+IB#A0s|=X2$C<&Vf1(iOcvxr74GQia^T-WjY-k zJ&OV}WOr7Mm$5frXX=&~t4Lk!fwK>seGiPt|IVq@b-Q45# z+Cf$8Pi$}G5+Jw+Qm5pcchkRV(kpe+FWep$dsNm`^X@oY9U-k&FIymUf}ym?qkw7vb1(?$ z696u^<fJ@KX1Dn&cb6=GfrE1Y32`iUn$(M`7e3A-O>O6 diff --git a/core/templates/base.html b/core/templates/base.html index f1305e7..1f614ac 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -33,17 +33,44 @@ + +
+
+
+
Estimated Cost
+ 0 workers × 0 days +
+

R 0.00

+
+
+
Cancel @@ -153,13 +164,7 @@ const teamId = this.value; if (teamId && teamWorkersMap[teamId]) { const workerIds = teamWorkersMap[teamId]; - // Uncheck all first? No, maybe append. Let's append as per common expectations unless explicit clear needed. - // Actually, if I change team, I probably expect to select THAT team's workers. - // Let's clear and select. - // But maybe I want to mix teams. - // User didn't specify. Previous logic was: select workers belonging to team. - // Let's stick to "select", don't uncheck others. - + // Select workers belonging to the team workerIds.forEach(function(id) { const checkbox = document.querySelector(`input[name="workers"][value="${id}"]`); if (checkbox) { @@ -176,6 +181,86 @@ var myModal = new bootstrap.Modal(conflictModalEl); myModal.show(); } + + // --- Cost Calculation Logic --- + const workerRates = {{ worker_rates_json|safe }}; + const startDateInput = document.getElementById('{{ form.date.id_for_label }}'); + const endDateInput = document.getElementById('{{ form.end_date.id_for_label }}'); + const satCheckbox = document.getElementById('{{ form.include_saturday.id_for_label }}'); + const sunCheckbox = document.getElementById('{{ form.include_sunday.id_for_label }}'); + const workerCheckboxes = document.querySelectorAll('input[name="workers"]'); + const totalDisplay = document.getElementById('estimatedTotal'); + const detailsDisplay = document.getElementById('calculationDetails'); + + function calculateTotal() { + // 1. Calculate Days + let days = 0; + const start = startDateInput.value ? new Date(startDateInput.value) : null; + const end = endDateInput.value ? new Date(endDateInput.value) : null; + + if (start) { + if (!end || end < start) { + days = 1; + } else { + // Iterate dates + let curr = new Date(start); + // Reset time components to avoid TZ issues + curr.setHours(0,0,0,0); + const last = new Date(end); + last.setHours(0,0,0,0); + + while (curr <= last) { + const dayOfWeek = curr.getDay(); // 0 = Sun, 6 = Sat + let isWorkingDay = true; + + if (dayOfWeek === 6 && !satCheckbox.checked) isWorkingDay = false; + if (dayOfWeek === 0 && !sunCheckbox.checked) isWorkingDay = false; + + if (isWorkingDay) days++; + + curr.setDate(curr.getDate() + 1); + } + } + } + + // 2. Sum Worker Rates + let dailyRateSum = 0; + let workerCount = 0; + + workerCheckboxes.forEach(cb => { + if (cb.checked) { + const rate = workerRates[cb.value] || 0; + dailyRateSum += rate; + workerCount++; + } + }); + + // 3. Update UI + const total = dailyRateSum * days; + totalDisplay.textContent = 'R ' + total.toLocaleString('en-ZA', {minimumFractionDigits: 2, maximumFractionDigits: 2}); + detailsDisplay.textContent = `${workerCount} worker${workerCount !== 1 ? 's' : ''} × ${days} day${days !== 1 ? 's' : ''}`; + } + + // Attach Listeners + if (startDateInput) startDateInput.addEventListener('change', calculateTotal); + if (endDateInput) endDateInput.addEventListener('change', calculateTotal); + if (satCheckbox) satCheckbox.addEventListener('change', calculateTotal); + if (sunCheckbox) sunCheckbox.addEventListener('change', calculateTotal); + + workerCheckboxes.forEach(cb => { + cb.addEventListener('change', calculateTotal); + }); + + // Also update when team changes (since it selects workers programmatically) + if (teamSelect) { + teamSelect.addEventListener('change', function() { + // Give it a moment for the check logic to finish + setTimeout(calculateTotal, 100); + }); + } + + // Initial Run + calculateTotal(); }); function submitConflict(action) { diff --git a/core/templates/core/work_log_list.html b/core/templates/core/work_log_list.html index 50974f9..fdd58ca 100644 --- a/core/templates/core/work_log_list.html +++ b/core/templates/core/work_log_list.html @@ -80,13 +80,24 @@
- {% if selected_worker or selected_team or selected_project or selected_payment_status and selected_payment_status != 'all' %} -
- - Clear all filters - + +
+ +
+
+ {% if selected_worker or selected_team or selected_project or selected_payment_status and selected_payment_status != 'all' %} + + Clear all filters + + {% else %} + Showing all records + {% endif %} +
+
+ Total Value + R {{ total_amount|floatformat:2 }} +
- {% endif %}
diff --git a/core/views.py b/core/views.py index 9c0cc92..655c5d2 100644 --- a/core/views.py +++ b/core/views.py @@ -24,6 +24,8 @@ def is_staff_or_supervisor(user): """Check if user is staff or manages at least one team/project.""" if user.is_staff or user.is_superuser: return True + if user.has_perm('core.view_project'): + return True return user.managed_teams.exists() or user.assigned_projects.exists() @login_required @@ -141,11 +143,16 @@ def log_attendance(request): conflicts.append(conflict_entry) if conflicts: + # Prepare worker rates for JS calculation + worker_qs = form.fields['workers'].queryset + worker_rates = {w.id: float(w.day_rate) for w in worker_qs} + context = { 'form': form, 'team_workers_json': json.dumps(team_workers_map), 'conflicting_workers': conflicts, 'is_conflict': True, + 'worker_rates_json': json.dumps(worker_rates), } return render(request, 'core/log_attendance.html', context) @@ -203,9 +210,14 @@ def log_attendance(request): else: form = WorkLogForm(user=request.user if request.user.is_authenticated else None) + # Pass worker rates for frontend total calculation + worker_qs = form.fields['workers'].queryset + worker_rates = {w.id: float(w.day_rate) for w in worker_qs} + context = { 'form': form, - 'team_workers_json': json.dumps(team_workers_map) + 'team_workers_json': json.dumps(team_workers_map), + 'worker_rates_json': json.dumps(worker_rates) } return render(request, 'core/log_attendance.html', context) @@ -831,4 +843,4 @@ def create_receipt(request): return render(request, 'core/create_receipt.html', { 'form': form, 'items': items - }) + }) \ No newline at end of file