Skip to content

Commit

Permalink
Merge pull request #439 from dimagi/hy/user-profile-changes
Browse files Browse the repository at this point in the history
User profile changes
  • Loading branch information
calellowitz authored Nov 21, 2024
2 parents 9484d55 + 1b57aec commit cc34026
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 80 deletions.
73 changes: 44 additions & 29 deletions commcare_connect/static/js/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,22 @@ window.circle = circle;
* @param {Array.<{lng: float, lat: float, precision: float}> visit_data - Visit location data for User
*/
function addAccuracyCircles(map, visit_data) {
map.on('load', () => {
const visit_accuracy_circles = [];
visit_data.forEach((loc) => {
visit_accuracy_circles.push(
circle([loc.lng, loc.lat], loc.precision, { units: 'meters' }),
);
const FILL_OPACITY = 0.1;
const OUTLINE_COLOR = '#fcbf49';
const OUTLINE_WIDTH = 3;
const OUTLINE_OPACITY = 0.5;

const visit_accuracy_circles = visit_data.map((loc) =>
circle([loc.lng, loc.lat], loc.precision, { units: 'meters' }),
);

// Check if the source exists, then update or add the source
if (map.getSource('visit_accuracy_circles')) {
map.getSource('visit_accuracy_circles').setData({
type: 'FeatureCollection',
features: visit_accuracy_circles,
});
} else {
map.addSource('visit_accuracy_circles', {
type: 'geojson',
data: {
Expand All @@ -45,38 +54,44 @@ function addAccuracyCircles(map, visit_data) {
type: 'fill',
paint: {
'fill-antialias': true,
'fill-opacity': 0.3,
'fill-opacity': FILL_OPACITY,
},
});

// Add the outline layer
map.addLayer({
id: 'visit-accuracy-circle-outlines-layer',
source: 'visit_accuracy_circles',
type: 'line',
paint: {
'line-color': '#fcbf49',
'line-width': 3,
'line-opacity': 0.5,
'line-color': OUTLINE_COLOR,
'line-width': OUTLINE_WIDTH,
'line-opacity': OUTLINE_OPACITY,
},
});
});
}
}

window.addAccuracyCircles = addAccuracyCircles;

function addCatchmentAreas(map, catchments) {
const ACTIVE_COLOR = '#3366ff';
const INACTIVE_COLOR = '#ff4d4d';
const CIRCLE_OPACITY = 0.3;
const CIRCLE_OPACITY = 0.15;

map.on('load', () => {
const catchmentCircles = catchments.map((catchment) =>
circle([catchment.lng, catchment.lat], catchment.radius, {
units: 'meters',
properties: { active: catchment.active },
}),
);
const catchmentCircles = catchments.map((catchment) =>
circle([catchment.lng, catchment.lat], catchment.radius, {
units: 'meters',
properties: { active: catchment.active },
}),
);

if (map.getSource('catchment_circles')) {
map.getSource('catchment_circles').setData({
type: 'FeatureCollection',
features: catchmentCircles,
});
} else {
map.addSource('catchment_circles', {
type: 'geojson',
data: {
Expand Down Expand Up @@ -105,17 +120,17 @@ function addCatchmentAreas(map, catchments) {
'line-opacity': 0.5,
},
});
}

if (catchments?.length) {
window.Alpine.nextTick(() => {
const legendElement = document.getElementById('legend');
if (legendElement) {
const legendData = window.Alpine.$data(legendElement);
legendData.show = true;
}
});
}
});
if (catchments?.length) {
window.Alpine.nextTick(() => {
const legendElement = document.getElementById('legend');
if (legendElement) {
const legendData = window.Alpine.$data(legendElement);
legendData.show = true;
}
});
}
}

window.addCatchmentAreas = addCatchmentAreas;
182 changes: 131 additions & 51 deletions commcare_connect/templates/opportunity/user_profile.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,112 @@
</ol>
</nav>

<div class="card w-50 mx-auto">
<div class="card-body">
<h1 class="display-4 mb-0 text-center">{{access.display_name}}</h1>
<div class="text-uppercase mb-2 text-center"><small>{{access.user.username}}</small></div>
<hr />
<div class="text-center">
<div class="row mb-1 pb-1">
<div class="col fw-bold">{% translate "Phone" %}</div>
<div class="col">{{access.user.phone_number}}</div>
</div>
<div class="row mb-1 pb-1">
<div class="col fw-bold">{% translate "Learn Progress" %}</div>
<div class="col">{{access.learn_progress}}%</div>
</div>
<div class="row mb-1 pb-1">
<div class="col fw-bold">{% translate "Total Visits" %}</div>
<div class="col">{{access.visit_count}}</div>
<div class="container-fluid py-4">
<div class="card shadow-sm">
<div class="card-body">
<!-- Profile Header -->
<div class="text-center mb-4">
<div class="bg-primary rounded-circle d-inline-flex align-items-center justify-content-center mb-3"
style="width: 80px; height: 80px;">
<span class="text-white display-6">{{access.display_name|slice:":1"}}</span>
</div>
<h1 class="h2 mb-1">{{access.display_name}}</h1>
<div class="text-muted"><small>{{access.user.username}}</small></div>
</div>
<div class="row">
<div class="col fw-bold">{% translate "Last Visit Date" %}</div>
<div class="col">{{access.last_visit_date}}</div>

<!-- Stats Grid -->
<div class="row g-3 mb-4">
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 bg-light">
<div class="card-body text-center">
<i class="bi bi-telephone mb-2 text-primary fs-4"></i>
<h6 class="text-muted mb-1">{% translate "Phone" %}</h6>
<div>{{access.user.phone_number}}</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 bg-light">
<div class="card-body text-center">
<i class="bi bi-graph-up mb-2 text-success fs-4"></i>
<h6 class="text-muted mb-1">{% translate "Learn Progress" %}</h6>
<div class="d-flex align-items-center justify-content-center">
<div class="progress w-75 bg-white">
<div class="progress-bar bg-success" role="progressbar"
style="width: {{access.learn_progress}}%" aria-valuenow="{{access.learn_progress}}"
aria-valuemin="0" aria-valuemax="100"></div>
</div>
<span class="ms-2">{{access.learn_progress}}%</span>
</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 bg-light">
<div class="card-body text-center">
<i class="bi bi-pin-map mb-2 text-info fs-4"></i>
<h6 class="text-muted mb-1">{% translate "Total Visits" %}</h6>
<div>{{access.visit_count}}</div>
</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 bg-light">
<div class="card-body text-center">
<i class="bi bi-calendar mb-2 text-warning fs-4"></i>
<h6 class="text-muted mb-1">{% translate "Last Visit" %}</h6>
<div>{{access.last_visit_date}}</div>
</div>
</div>
</div>
</div>
</div>
<div class="position-relative">
<div id="user-visit-map" class="mt-3 mb-2 rounded border" style="height: 300px;"></div>
<div id="legend" class="card position-absolute bottom-0 end-0 m-2 z-1" x-data="{ show: false}" x-show="show">
<div class="card-body">
<h6 class="card-title">Catchment Areas</h6>
<div class="mb-2 d-flex align-items-center">
<span class="d-inline-block me-2"
style="width: 20px; height: 20px; opacity: 0.3; background-color: #3366ff;"></span>
<span>Active</span>

<!-- Map Section -->
<div class="card position-relative" x-data="{ currentStyle: 'streets-v12' }">
<div class="card-header bg-light d-flex justify-content-between align-items-center">
<h5 class="mb-0">Visit Locations</h5>
<div class="btn-group btn-group-sm">
<button type="button" @click="currentStyle = 'streets-v12'" class="btn btn-outline-primary" :class="{ 'active pe-none': currentStyle === 'streets-v12' }" id="streets-v12">
<i class="bi bi-map me-1"></i>Streets
</button>
<button type="button" @click="currentStyle = 'satellite-streets-v12'" class="btn btn-outline-primary" :class="{ 'active pe-none': currentStyle === 'satellite-streets-v12' }" id="satellite-streets-v12">
<i class="bi bi-globe me-1"></i>Satellite
</button>
</div>
</div>
<div class="d-flex align-items-center">
<span class="d-inline-block me-2"
style="width: 20px; height: 20px; opacity: 0.3; background-color: #ff4d4d;"></span>
<span>Inactive</span>
<div class="card-body p-0">
<div id="user-visit-map" class="w-100" style="height: 600px;"></div>
<div id="legend" class="card position-absolute bottom-0 end-0 m-3 shadow-sm" x-data="{ show: false}" x-show="show">
<div class="card-body p-3">
<h6 class="card-title mb-3">Catchment Areas</h6>
<div class="mb-2 d-flex align-items-center">
<span class="d-inline-block me-2 rounded"
style="width: 20px; height: 20px; opacity: 0.3; background-color: #3366ff;"></span>
<span>Active</span>
</div>
<div class="d-flex align-items-center">
<span class="d-inline-block me-2 rounded"
style="width: 20px; height: 20px; opacity: 0.3; background-color: #ff4d4d;"></span>
<span>Inactive</span>
</div>
</div>
</div>
</div>
</div>

<!-- Action Buttons -->
<div class="text-center mt-4">
{% if access.suspended %}
<a class="btn btn-success btn-lg" href="{% url "opportunity:revoke_user_suspension" org_slug=request.org opp_id=access.opportunity_id pk=access.pk %}?next={{ request.path|urlencode }}">
<i class="bi bi-person-check me-1"></i>{% translate "Revoke Suspension" %}
</a>
{% else %}
<button class="btn btn-danger btn-lg" data-bs-toggle="modal" data-bs-target="#suspendUserModal">
<i class="bi bi-person-x me-1"></i>{% translate "Suspend" %}
</button>
{% endif %}
</div>
</div>
{% if access.suspended %}
<a class="btn btn-success"
href="{% url "opportunity:revoke_user_suspension" org_slug=request.org opp_id=access.opportunity_id pk=access.pk %}?next={{ request.path|urlencode }}">
{% translate "Revoke Suspension" %}
</a>
{% else %}
<button class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#suspendUserModal">{% translate "Suspend" %}</button>
{% endif %}
</div>
</div>
{% endblock content %}
Expand All @@ -84,20 +141,43 @@ <h6 class="card-title">Catchment Areas</h6>
mapboxgl.accessToken = "{{ MAPBOX_TOKEN }}";
const map = new mapboxgl.Map({
container: 'user-visit-map',
style: 'mapbox://styles/mapbox/satellite-streets-v12',
style: 'mapbox://styles/mapbox/streets-v12',
center: [{{ lng_avg }}, {{ lat_avg }}],
zoom: 14,
});

const userVisits = JSON.parse(document.getElementById('userVisits').textContent);
userVisits.forEach(loc => {
new mapboxgl.Marker()
.setLngLat([loc.lng, loc.lat])
.setPopup(new mapboxgl.Popup().setHTML(`${loc.entity_name}<br />${loc.visit_date}`))
.addTo(map)
})
addAccuracyCircles(map, userVisits);
const userCatchments = JSON.parse(document.getElementById('userCatchments').textContent);
addCatchmentAreas(map, userCatchments)

map.on('load', () => {
userVisits.forEach(loc => {
new mapboxgl.Marker()
.setLngLat([loc.lng, loc.lat])
.setPopup(new mapboxgl.Popup().setHTML(`${loc.entity_name}<br />${loc.visit_date}`))
.addTo(map)
});

addAccuracyCircles(map, userVisits);
addCatchmentAreas(map, userCatchments);
});

// Watch for Alpine.js style changes
Alpine.effect(() => {
const alpineData = Alpine.$data(document.querySelector('[x-data]'));
const currentStyle = alpineData.currentStyle;
const styles = {
'streets-v12': 'mapbox://styles/mapbox/streets-v12',
'satellite-streets-v12': 'mapbox://styles/mapbox/satellite-streets-v12'
};
map.setStyle(styles[currentStyle]);

// Re-add circles and catchments after style changes
map.once('style.load', () => {
alpineData.currentStyle = currentStyle;
addAccuracyCircles(map, userVisits);
addCatchmentAreas(map, userCatchments);
});
});
});
</script>
{% endblock %}
Expand Down

0 comments on commit cc34026

Please sign in to comment.