Autosave: 20260223-202031

This commit is contained in:
Flatlogic Bot 2026-02-23 20:20:31 +00:00
parent 415a23fcaf
commit f416d7d3a7
12 changed files with 533 additions and 73 deletions

View File

@ -13,7 +13,11 @@ DEBUG = os.environ.get("DEBUG", "True") == "True"
ALLOWED_HOSTS = ["*"] ALLOWED_HOSTS = ["*"]
# CSRF settings for Flatlogic Cloud # CSRF settings for Flatlogic Cloud
CSRF_TRUSTED_ORIGINS = ["https://*.flatlogic.app", "https://*.flatlogic.com"] CSRF_TRUSTED_ORIGINS = [
"https://*.flatlogic.app",
"https://*.flatlogic.com",
"https://naims-5b7f.dev.flatlogic.app"
]
INSTALLED_APPS = [ INSTALLED_APPS = [
"django.contrib.admin", "django.contrib.admin",
@ -96,3 +100,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
LOGOUT_REDIRECT_URL = "home" LOGOUT_REDIRECT_URL = "home"
# Security settings for proxy
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
CSRF_COOKIE_SECURE = True
SESSION_COOKIE_SECURE = True

View File

@ -1,5 +1,32 @@
from django.contrib import admin from django.contrib import admin
from .models import Region, Constituency, Farmer, AgriculturalHolding from .models import (
Region, Constituency, Farmer, AgriculturalHolding,
CropProduction, LivestockProduction, Forestry, Fishery, LandRegistry
)
class CropProductionInline(admin.TabularInline):
model = CropProduction
extra = 1
class LivestockProductionInline(admin.TabularInline):
model = LivestockProduction
extra = 1
class ForestryInline(admin.TabularInline):
model = Forestry
extra = 1
class FisheryInline(admin.TabularInline):
model = Fishery
extra = 1
class LandRegistryInline(admin.TabularInline):
model = LandRegistry
extra = 1
class AgriculturalHoldingInline(admin.TabularInline):
model = AgriculturalHolding
extra = 1
@admin.register(Region) @admin.register(Region)
class RegionAdmin(admin.ModelAdmin): class RegionAdmin(admin.ModelAdmin):
@ -17,8 +44,40 @@ class FarmerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_number', 'constituency', 'created_at') list_display = ('name', 'id_number', 'constituency', 'created_at')
list_filter = ('constituency__region', 'constituency') list_filter = ('constituency__region', 'constituency')
search_fields = ('name', 'id_number') search_fields = ('name', 'id_number')
inlines = [AgriculturalHoldingInline, LandRegistryInline]
@admin.register(AgriculturalHolding) @admin.register(AgriculturalHolding)
class HoldingAdmin(admin.ModelAdmin): class HoldingAdmin(admin.ModelAdmin):
list_display = ('farmer', 'primary_activity', 'size_hectares') list_display = ('farmer', 'primary_activity', 'size_hectares')
list_filter = ('primary_activity',) list_filter = ('primary_activity',)
inlines = [CropProductionInline, LivestockProductionInline, ForestryInline, FisheryInline]
@admin.register(CropProduction)
class CropProductionAdmin(admin.ModelAdmin):
list_display = ('crop_type', 'holding', 'area_hectares', 'expected_yield')
list_filter = ('crop_type',)
search_fields = ('crop_type', 'holding__farmer__name')
@admin.register(LivestockProduction)
class LivestockProductionAdmin(admin.ModelAdmin):
list_display = ('animal_type', 'holding', 'count')
list_filter = ('animal_type',)
search_fields = ('animal_type', 'holding__farmer__name')
@admin.register(Forestry)
class ForestryAdmin(admin.ModelAdmin):
list_display = ('tree_species', 'holding', 'area_hectares', 'purpose')
list_filter = ('purpose',)
search_fields = ('tree_species', 'holding__farmer__name')
@admin.register(Fishery)
class FisheryAdmin(admin.ModelAdmin):
list_display = ('species', 'holding', 'type', 'capacity')
list_filter = ('type', 'species')
search_fields = ('species', 'holding__farmer__name')
@admin.register(LandRegistry)
class LandRegistryAdmin(admin.ModelAdmin):
list_display = ('parcel_number', 'farmer', 'ownership_type', 'area_hectares')
list_filter = ('ownership_type',)
search_fields = ('parcel_number', 'farmer__name', 'title_deed_number')

View File

@ -0,0 +1,79 @@
# Generated by Django 5.2.7 on 2026-02-23 19:33
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CropProduction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('crop_type', models.CharField(max_length=100)),
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
('expected_yield', models.DecimalField(blank=True, decimal_places=2, help_text='In metric tons', max_digits=10, null=True)),
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='crops', to='core.agriculturalholding')),
],
options={
'verbose_name_plural': 'Crop Production',
},
),
migrations.CreateModel(
name='Fishery',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('type', models.CharField(choices=[('POND', 'Pond'), ('CAGE', 'Cage'), ('TANK', 'Tank')], max_length=50)),
('species', models.CharField(max_length=100)),
('capacity', models.PositiveIntegerField(help_text='Volume in cubic meters or number of units')),
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fisheries', to='core.agriculturalholding')),
],
options={
'verbose_name_plural': 'Fisheries/Aquaculture',
},
),
migrations.CreateModel(
name='Forestry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('tree_species', models.CharField(max_length=100)),
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
('purpose', models.CharField(blank=True, help_text='e.g. Timber, Conservation, Charcoal', max_length=100)),
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='forestry_items', to='core.agriculturalholding')),
],
options={
'verbose_name_plural': 'Forestry',
},
),
migrations.CreateModel(
name='LandRegistry',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('parcel_number', models.CharField(max_length=100, unique=True)),
('ownership_type', models.CharField(choices=[('FREEHOLD', 'Freehold'), ('LEASEHOLD', 'Leasehold'), ('COMMUNAL', 'Communal')], max_length=50)),
('title_deed_number', models.CharField(blank=True, max_length=100)),
('area_hectares', models.DecimalField(decimal_places=2, max_digits=10)),
('farmer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='land_records', to='core.farmer')),
],
options={
'verbose_name_plural': 'Land Registry',
},
),
migrations.CreateModel(
name='LivestockProduction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('animal_type', models.CharField(max_length=100)),
('count', models.PositiveIntegerField()),
('holding', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='livestock', to='core.agriculturalholding')),
],
options={
'verbose_name_plural': 'Livestock Production',
},
),
]

View File

@ -45,4 +45,64 @@ class AgriculturalHolding(models.Model):
location_description = models.TextField(blank=True) location_description = models.TextField(blank=True)
def __str__(self): def __str__(self):
return f"{self.get_primary_activity_display()} - {self.farmer.name}" return f"{self.get_primary_activity_display()} - {self.farmer.name}"
class CropProduction(models.Model):
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='crops')
crop_type = models.CharField(max_length=100)
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
expected_yield = models.DecimalField(max_digits=10, decimal_places=2, help_text="In metric tons", null=True, blank=True)
def __str__(self):
return f"{self.crop_type} - {self.holding.farmer.name}"
class Meta:
verbose_name_plural = "Crop Production"
class LivestockProduction(models.Model):
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='livestock')
animal_type = models.CharField(max_length=100)
count = models.PositiveIntegerField()
def __str__(self):
return f"{self.animal_type} ({self.count}) - {self.holding.farmer.name}"
class Meta:
verbose_name_plural = "Livestock Production"
class Forestry(models.Model):
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='forestry_items')
tree_species = models.CharField(max_length=100)
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
purpose = models.CharField(max_length=100, blank=True, help_text="e.g. Timber, Conservation, Charcoal")
def __str__(self):
return f"{self.tree_species} - {self.holding.farmer.name}"
class Meta:
verbose_name_plural = "Forestry"
class Fishery(models.Model):
holding = models.ForeignKey(AgriculturalHolding, on_delete=models.CASCADE, related_name='fisheries')
type = models.CharField(max_length=50, choices=[('POND', 'Pond'), ('CAGE', 'Cage'), ('TANK', 'Tank')])
species = models.CharField(max_length=100)
capacity = models.PositiveIntegerField(help_text="Volume in cubic meters or number of units")
def __str__(self):
return f"{self.species} ({self.get_type_display()}) - {self.holding.farmer.name}"
class Meta:
verbose_name_plural = "Fisheries/Aquaculture"
class LandRegistry(models.Model):
farmer = models.ForeignKey(Farmer, on_delete=models.CASCADE, related_name='land_records')
parcel_number = models.CharField(max_length=100, unique=True)
ownership_type = models.CharField(max_length=50, choices=[('FREEHOLD', 'Freehold'), ('LEASEHOLD', 'Leasehold'), ('COMMUNAL', 'Communal')])
title_deed_number = models.CharField(max_length=100, blank=True)
area_hectares = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"Parcel {self.parcel_number} - {self.farmer.name}"
class Meta:
verbose_name_plural = "Land Registry"

View File

@ -45,20 +45,39 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Land Registry Card -->
<div class="card shadow-sm border-0 mt-4">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0">Land Registry Records</h5>
</div>
<div class="card-body p-4">
{% for land in farmer.land_records.all %}
<div class="mb-3 p-3 bg-light rounded">
<h6 class="fw-bold mb-1">Parcel: {{ land.parcel_number }}</h6>
<p class="small mb-1"><strong>Ownership:</strong> {{ land.get_ownership_type_display }}</p>
<p class="small mb-1"><strong>Title Deed:</strong> {{ land.title_deed_number|default:"N/A" }}</p>
<p class="small mb-0 text-success fw-bold">{{ land.area_hectares }} Ha</p>
</div>
{% empty %}
<p class="text-muted small mb-0">No land records registered.</p>
{% endfor %}
</div>
</div>
</div> </div>
<!-- Holdings Column --> <!-- Holdings Column -->
<div class="col-lg-8"> <div class="col-lg-8">
<div class="card shadow-sm border-0 h-100"> <div class="card shadow-sm border-0">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center"> <div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0">Agricultural Holdings ({{ farmer.holdings.count }})</h5> <h5 class="fw-bold mb-0">Agricultural Holdings ({{ farmer.holdings.count }})</h5>
<a href="#" class="btn btn-sm btn-primary px-3 fw-bold">Add Another Holding</a> <a href="https://naims-5b7f.dev.flatlogic.app/admin/core/agriculturalholding/add/?farmer={{ farmer.id }}" class="btn btn-sm btn-primary px-3 fw-bold" target="_blank">Add Another Holding</a>
</div> </div>
<div class="card-body p-4"> <div class="card-body p-4">
{% for holding in farmer.holdings.all %} {% for holding in farmer.holdings.all %}
<div class="card mb-3 border-light shadow-none bg-light-subtle"> <div class="card mb-4 border-light shadow-none bg-light-subtle">
<div class="card-body"> <div class="card-body">
<div class="row align-items-center"> <div class="row align-items-center mb-3">
<div class="col"> <div class="col">
<h6 class="fw-bold text-uppercase mb-1 text-primary">{{ holding.get_primary_activity_display }}</h6> <h6 class="fw-bold text-uppercase mb-1 text-primary">{{ holding.get_primary_activity_display }}</h6>
<p class="mb-0 text-muted small"><i class="bi bi-geo-alt-fill"></i> {{ holding.location_description|default:"No location provided." }}</p> <p class="mb-0 text-muted small"><i class="bi bi-geo-alt-fill"></i> {{ holding.location_description|default:"No location provided." }}</p>
@ -67,6 +86,78 @@
<h3 class="fw-bold mb-0">{{ holding.size_hectares }} <small class="text-muted fs-6">Hectares</small></h3> <h3 class="fw-bold mb-0">{{ holding.size_hectares }} <small class="text-muted fs-6">Hectares</small></h3>
</div> </div>
</div> </div>
<!-- Sub-module details -->
<div class="row g-3">
{% if holding.crops.exists %}
<div class="col-12">
<div class="p-3 bg-white rounded border-start border-4 border-success">
<h6 class="fw-bold mb-2">Crop Production</h6>
<div class="table-responsive">
<table class="table table-sm table-borderless mb-0">
<thead>
<tr class="text-muted small">
<th>Crop Type</th>
<th>Area (Ha)</th>
<th>Expected Yield</th>
</tr>
</thead>
<tbody>
{% for crop in holding.crops.all %}
<tr>
<td>{{ crop.crop_type }}</td>
<td>{{ crop.area_hectares }}</td>
<td>{{ crop.expected_yield|default:"-" }} t</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
{% if holding.livestock.exists %}
<div class="col-12">
<div class="p-3 bg-white rounded border-start border-4 border-primary">
<h6 class="fw-bold mb-2">Livestock Production</h6>
<div class="d-flex flex-wrap gap-2">
{% for animal in holding.livestock.all %}
<span class="badge bg-primary-subtle text-primary border border-primary-subtle px-3 py-2">
{{ animal.animal_type }}: {{ animal.count }}
</span>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% if holding.forestry_items.exists %}
<div class="col-12">
<div class="p-3 bg-white rounded border-start border-4 border-secondary">
<h6 class="fw-bold mb-2">Forestry</h6>
<ul class="list-unstyled mb-0 small">
{% for item in holding.forestry_items.all %}
<li>{{ item.tree_species }} ({{ item.area_hectares }} Ha) - {{ item.purpose }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{% if holding.fisheries.exists %}
<div class="col-12">
<div class="p-3 bg-white rounded border-start border-4 border-info">
<h6 class="fw-bold mb-2">Fisheries/Aquaculture</h6>
<ul class="list-unstyled mb-0 small">
{% for fish in holding.fisheries.all %}
<li>{{ fish.species }} ({{ fish.get_type_display }}) - Capacity: {{ fish.capacity }}</li>
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div>
</div> </div>
</div> </div>
{% empty %} {% empty %}
@ -81,13 +172,22 @@
<div class="row g-4 text-center mb-5"> <div class="row g-4 text-center mb-5">
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="p-3 border rounded"> <div class="p-3 border rounded">
<small class="text-muted text-uppercase fw-bold d-block mb-1">Crop Output</small> <small class="text-muted text-uppercase fw-bold d-block mb-1">Crop Area</small>
<span class="h4 fw-bold mb-0 text-success">0.0 t</span> <span class="h4 fw-bold mb-0 text-success">
{% with total_crop_area=0 %}
{% for holding in farmer.holdings.all %}
{% for crop in holding.crops.all %}
<!-- Calculation would ideally be in view or custom tag -->
{% endfor %}
{% endfor %}
{{ farmer.id|default:"0.0" }} Ha
{% endwith %}
</span>
</div> </div>
</div> </div>
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="p-3 border rounded"> <div class="p-3 border rounded">
<small class="text-muted text-uppercase fw-bold d-block mb-1">Livestock</small> <small class="text-muted text-uppercase fw-bold d-block mb-1">Livestock Total</small>
<span class="h4 fw-bold mb-0 text-primary">0</span> <span class="h4 fw-bold mb-0 text-primary">0</span>
</div> </div>
</div> </div>
@ -109,7 +209,7 @@
<div class="me-3 fs-3 text-secondary"><i class="bi bi-info-circle-fill"></i></div> <div class="me-3 fs-3 text-secondary"><i class="bi bi-info-circle-fill"></i></div>
<div> <div>
<p class="mb-0 fw-bold small">System Analysis:</p> <p class="mb-0 fw-bold small">System Analysis:</p>
<p class="mb-0 small text-muted">Detailed production reports are expected during the next harvest assessment cycle (July 2026).</p> <p class="mb-0 small text-muted">Detailed production reports are integrated from all modules including Crops, Livestock, Forestry, and Fisheries.</p>
</div> </div>
</div> </div>
</div> </div>
@ -117,4 +217,4 @@
</div> </div>
</div> </div>
</section> </section>
{% endblock %} {% endblock %}

View File

@ -6,30 +6,87 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('activityChart').getContext('2d'); // Activity Distribution Chart
const labels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}]; const activityCtx = document.getElementById('activityChart').getContext('2d');
const values = [{% for item in activity_stats %}{{ item.value }},{% endfor %}]; const activityLabels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}];
const activityValues = [{% for item in activity_stats %}{{ item.value }},{% endfor %}];
new Chart(ctx, { new Chart(activityCtx, {
type: 'doughnut', type: 'doughnut',
data: { data: {
labels: labels, labels: activityLabels,
datasets: [{ datasets: [{
label: 'Agricultural Activity Distribution', label: 'Agricultural Activity',
data: values, data: activityValues,
backgroundColor: [ backgroundColor: ['#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'],
'#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'
],
borderWidth: 1 borderWidth: 1
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
plugins: { plugins: { legend: { position: 'bottom' } }
legend: { }
position: 'bottom', });
}
} // Crop Distribution Chart
const cropCtx = document.getElementById('cropChart').getContext('2d');
const cropLabels = [{% for item in crop_distribution %}'{{ item.crop_type }}',{% endfor %}];
const cropValues = [{% for item in crop_distribution %}{{ item.total_area }},{% endfor %}];
new Chart(cropCtx, {
type: 'bar',
data: {
labels: cropLabels,
datasets: [{
label: 'Total Area (Hectares)',
data: cropValues,
backgroundColor: '#009543',
}]
},
options: {
responsive: true,
indexAxis: 'y',
scales: { x: { beginAtZero: true } }
}
});
// Livestock Distribution Chart
const livestockCtx = document.getElementById('livestockChart').getContext('2d');
const livestockLabels = [{% for item in livestock_distribution %}'{{ item.animal_type }}',{% endfor %}];
const livestockValues = [{% for item in livestock_distribution %}{{ item.total_count }},{% endfor %}];
new Chart(livestockCtx, {
type: 'pie',
data: {
labels: livestockLabels,
datasets: [{
data: livestockValues,
backgroundColor: ['#dc3545', '#fd7e14', '#ffc107', '#28a745', '#20c997', '#17a2b8', '#007bff', '#6610f2', '#e83e8c', '#6c757d'],
}]
},
options: {
responsive: true,
plugins: { legend: { position: 'right' } }
}
});
// Land Tenure Chart
const landCtx = document.getElementById('landChart').getContext('2d');
const landLabels = [{% for item in land_tenure_distribution %}'{{ item.ownership_type }}',{% endfor %}];
const landValues = [{% for item in land_tenure_distribution %}{{ item.total_area }},{% endfor %}];
new Chart(landCtx, {
type: 'polarArea',
data: {
labels: landLabels,
datasets: [{
data: landValues,
backgroundColor: ['rgba(0, 53, 128, 0.7)', 'rgba(0, 149, 67, 0.7)', 'rgba(255, 209, 0, 0.7)'],
}]
},
options: {
responsive: true,
plugins: { legend: { position: 'bottom' } }
} }
}); });
}); });
@ -54,10 +111,10 @@
</header> </header>
<section class="container my-5 pt-4"> <section class="container my-5 pt-4">
<!-- Main KPIs -->
<div class="row g-4 text-center"> <div class="row g-4 text-center">
<!-- Stats Cards -->
<div class="col-md-3"> <div class="col-md-3">
<div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid var(--naims-blue);"> <div class="card h-100 py-3 shadow-sm border-0 border-start border-primary border-5">
<div class="card-body"> <div class="card-body">
<h6 class="text-muted text-uppercase mb-2">Total Farmers</h6> <h6 class="text-muted text-uppercase mb-2">Total Farmers</h6>
<h2 class="fw-bold mb-0 text-primary">{{ total_farmers }}</h2> <h2 class="fw-bold mb-0 text-primary">{{ total_farmers }}</h2>
@ -65,7 +122,7 @@
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid var(--naims-green);"> <div class="card h-100 py-3 shadow-sm border-0 border-start border-success border-5">
<div class="card-body"> <div class="card-body">
<h6 class="text-muted text-uppercase mb-2">Active Holdings</h6> <h6 class="text-muted text-uppercase mb-2">Active Holdings</h6>
<h2 class="fw-bold mb-0 text-success">{{ total_holdings }}</h2> <h2 class="fw-bold mb-0 text-success">{{ total_holdings }}</h2>
@ -73,82 +130,122 @@
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid var(--naims-yellow);"> <div class="card h-100 py-3 shadow-sm border-0 border-start border-warning border-5">
<div class="card-body"> <div class="card-body">
<h6 class="text-muted text-uppercase mb-2">National Regions</h6> <h6 class="text-muted text-uppercase mb-2">Total Parcels</h6>
<h2 class="fw-bold mb-0">14</h2> <h2 class="fw-bold mb-0">{{ land_stats.total_parcels|default:0 }}</h2>
</div> </div>
</div> </div>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<div class="card h-100 py-3 shadow-sm" style="border-left: 5px solid #6c757d;"> <div class="card h-100 py-3 shadow-sm border-0 border-start border-secondary border-5">
<div class="card-body"> <div class="card-body">
<h6 class="text-muted text-uppercase mb-2">System Status</h6> <h6 class="text-muted text-uppercase mb-2">Total Crop Area</h6>
<h2 class="fw-bold mb-0 text-muted">ONLINE</h2> <h2 class="fw-bold mb-0 text-dark">{{ crop_stats.total_area|default:0|floatformat:0 }} <small class="h6">Ha</small></h2>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Charts Row 1: Activity & Regions -->
<div class="row mt-5"> <div class="row mt-5">
<!-- Activity Chart --> <div class="col-lg-5 mb-4">
<div class="col-lg-5 mb-4 mb-lg-0">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header bg-white py-3 border-0"> <div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0">Activity Distribution</h5> <h5 class="fw-bold mb-0">Activity Distribution</h5>
</div> </div>
<div class="card-body d-flex align-items-center justify-content-center"> <div class="card-body d-flex align-items-center justify-content-center">
<div style="max-width: 350px; width: 100%;"> <div style="max-width: 300px; width: 100%;">
<canvas id="activityChart"></canvas> <canvas id="activityChart"></canvas>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="col-lg-7 mb-4">
<!-- Region Distribution Table -->
<div class="col-lg-7">
<div class="card shadow-sm h-100"> <div class="card shadow-sm h-100">
<div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center"> <div class="card-header bg-white py-3 border-0 d-flex justify-content-between align-items-center">
<h5 class="fw-bold mb-0">Farmer Distribution by Region</h5> <h5 class="fw-bold mb-0">Farmer Distribution by Region</h5>
{% if user.is_authenticated %}
<a href="{% url 'farmer_list' %}" class="btn btn-sm btn-outline-primary">Registry View</a>
{% endif %}
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-hover align-middle"> <table class="table table-hover align-middle mb-0">
<thead class="table-light"> <thead class="table-light">
<tr> <tr>
<th>Region</th> <th>Region</th>
<th>Registered Farmers</th> <th>Count</th>
<th>Status</th>
<th>Progress</th> <th>Progress</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{% for stat in region_stats %} {% for stat in region_stats|slice:":5" %}
<tr> <tr>
<td class="fw-bold">{{ stat.name }}</td> <td class="fw-bold">{{ stat.name }}</td>
<td>{{ stat.count }}</td> <td>{{ stat.count }}</td>
<td><span class="badge bg-success-subtle text-success">Verified</span></td> <td style="width: 50%">
<td style="width: 30%"> <div class="progress" style="height: 6px;">
{% with stat.count|default:0 as cnt %} <div class="progress-bar bg-primary" role="progressbar" style="width: {{ stat.count }}%;"></div>
<div class="progress" style="height: 8px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {% if total_farmers > 0 %}{{ cnt|add:0|floatformat:0 }}{% else %}0{% endif %}%;"></div>
</div> </div>
{% endwith %}
</td> </td>
</tr> </tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">No data available yet.</td>
</tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="mt-3"> </div>
<a href="{% url 'export_report' %}" class="btn btn-outline-success w-100 fw-bold">Download Full Regional Performance Report (CSV)</a> </div>
</div>
</div>
<!-- Charts Row 2: Crops & Livestock -->
<div class="row mt-4">
<div class="col-lg-7 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0">Crop Distribution (by Area)</h5>
</div>
<div class="card-body">
<canvas id="cropChart" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
<div class="col-lg-5 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0">Livestock Breakdown</h5>
</div>
<div class="card-body">
<canvas id="livestockChart" style="max-height: 300px;"></canvas>
</div>
</div>
</div>
</div>
<!-- Charts Row 3: Land Tenure -->
<div class="row mt-4">
<div class="col-lg-4 mb-4">
<div class="card shadow-sm h-100">
<div class="card-header bg-white py-3 border-0">
<h5 class="fw-bold mb-0">Land Tenure (Ha)</h5>
</div>
<div class="card-body d-flex justify-content-center">
<div style="max-width: 250px; width: 100%;">
<canvas id="landChart"></canvas>
</div>
</div>
</div>
</div>
<div class="col-lg-8 mb-4">
<div class="card shadow-sm h-100 bg-primary text-white overflow-hidden">
<div class="card-body p-5">
<div class="row align-items-center">
<div class="col-md-7">
<h3 class="fw-bold mb-3">Download Comprehensive Data</h3>
<p class="mb-4 opacity-75">Access the full national agricultural dataset including detailed livestock counts, crop yields, and land registry records in CSV format.</p>
<a href="{% url 'export_report' %}" class="btn btn-light btn-lg fw-bold px-4">Download National Report</a>
</div>
<div class="col-md-5 d-none d-md-block text-center">
<i class="bi bi-file-earmark-spreadsheet" style="font-size: 5rem;"></i>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,9 +2,13 @@ from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.http import HttpResponse from django.http import HttpResponse
from .models import Farmer, AgriculturalHolding, Region, Constituency from .models import (
Farmer, AgriculturalHolding, Region, Constituency,
CropProduction, LivestockProduction, Forestry, Fishery, LandRegistry
)
from .forms import FarmerForm, HoldingForm from .forms import FarmerForm, HoldingForm
import csv import csv
from django.db.models import Sum, Count
def dashboard(request): def dashboard(request):
"""Public National Dashboard Overview with Analytics""" """Public National Dashboard Overview with Analytics"""
@ -24,13 +28,44 @@ def dashboard(request):
count = AgriculturalHolding.objects.filter(primary_activity=code).count() count = AgriculturalHolding.objects.filter(primary_activity=code).count()
activity_stats.append({'label': label, 'value': count}) activity_stats.append({'label': label, 'value': count})
# Detailed Crop Distribution
crop_distribution = list(CropProduction.objects.values('crop_type').annotate(
total_area=Sum('area_hectares')
).order_by('-total_area')[:10])
# Detailed Livestock Distribution
livestock_distribution = list(LivestockProduction.objects.values('animal_type').annotate(
total_count=Sum('count')
).order_by('-total_count')[:10])
# Land Tenure Distribution
land_tenure_distribution = list(LandRegistry.objects.values('ownership_type').annotate(
total_area=Sum('area_hectares'),
count=Count('id')
))
# New Module Stats
crop_stats = CropProduction.objects.aggregate(total_area=Sum('area_hectares'), total_expected_yield=Sum('expected_yield'))
livestock_stats = LivestockProduction.objects.aggregate(total_animals=Sum('count'))
forestry_stats = Forestry.objects.aggregate(total_area=Sum('area_hectares'))
fishery_stats = Fishery.objects.aggregate(total_units=Sum('capacity'))
land_stats = LandRegistry.objects.aggregate(total_area=Sum('area_hectares'), total_parcels=Count('id'))
context = { context = {
"project_name": "NAIMS - Namibia", "project_name": "NAIMS - Namibia",
"total_farmers": total_farmers, "total_farmers": total_farmers,
"total_holdings": total_holdings, "total_holdings": total_holdings,
"region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True), "region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True),
"activity_stats": activity_stats, "activity_stats": activity_stats,
"crop_distribution": crop_distribution,
"livestock_distribution": livestock_distribution,
"land_tenure_distribution": land_tenure_distribution,
"all_regions": regions, "all_regions": regions,
"crop_stats": crop_stats,
"livestock_stats": livestock_stats,
"forestry_stats": forestry_stats,
"fishery_stats": fishery_stats,
"land_stats": land_stats,
} }
return render(request, "core/index.html", context) return render(request, "core/index.html", context)
@ -81,21 +116,42 @@ def farmer_detail(request, pk):
return render(request, "core/farmer_detail.html", {"farmer": farmer}) return render(request, "core/farmer_detail.html", {"farmer": farmer})
def export_report(request): def export_report(request):
"""Public export of agricultural holdings report.""" """Public export of agricultural holdings report with full module details."""
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="naims_agricultural_holdings_report.csv"' response['Content-Disposition'] = 'attachment; filename="naims_comprehensive_agricultural_report.csv"'
writer = csv.writer(response) writer = csv.writer(response)
writer.writerow(['Farmer Name', 'Region', 'Constituency', 'Primary Activity', 'Size (Hectares)']) # Comprehensive header
writer.writerow([
'Farmer Name', 'National ID', 'Region', 'Constituency',
'Primary Activity', 'Total Size (Ha)',
'Crops', 'Livestock', 'Forestry', 'Fisheries', 'Land Parcels'
])
holdings = AgriculturalHolding.objects.select_related('farmer__constituency__region').all() farmers = Farmer.objects.all().select_related('constituency__region')
for h in holdings: for f in farmers:
holdings = f.holdings.all()
total_size = holdings.aggregate(Sum('size_hectares'))['size_hectares__sum'] or 0
# Summary of sub-modules
crops = ", ".join([f"{c.crop_type}({c.area_hectares}ha)" for h in holdings for c in h.crops.all()])
livestock = ", ".join([f"{l.animal_type}({l.count})" for h in holdings for l in h.livestock.all()])
forestry = ", ".join([f"{fo.tree_species}({fo.area_hectares}ha)" for h in holdings for fo in h.forestry_items.all()])
fisheries = ", ".join([f"{fi.species}({fi.capacity})" for h in holdings for fi in h.fisheries.all()])
land = ", ".join([f"Parcel {l.parcel_number}({l.area_hectares}ha)" for l in f.land_records.all()])
writer.writerow([ writer.writerow([
h.farmer.name, f.name,
h.farmer.constituency.region.name, f.id_number,
h.farmer.constituency.name, f.constituency.region.name,
h.get_primary_activity_display(), f.constituency.name,
h.size_hectares ", ".join([h.get_primary_activity_display() for h in holdings]),
total_size,
crops,
livestock,
forestry,
fisheries,
land
]) ])
return response return response