Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

User profile changes #439

Merged
merged 5 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading