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 = ["*"]
# 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 = [
"django.contrib.admin",
@ -96,3 +100,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
LOGIN_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 .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)
class RegionAdmin(admin.ModelAdmin):
@ -17,8 +44,40 @@ class FarmerAdmin(admin.ModelAdmin):
list_display = ('name', 'id_number', 'constituency', 'created_at')
list_filter = ('constituency__region', 'constituency')
search_fields = ('name', 'id_number')
inlines = [AgriculturalHoldingInline, LandRegistryInline]
@admin.register(AgriculturalHolding)
class HoldingAdmin(admin.ModelAdmin):
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)
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>
<!-- 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>
<!-- Holdings Column -->
<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">
<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 class="card-body p-4">
{% 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="row align-items-center">
<div class="row align-items-center mb-3">
<div class="col">
<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>
@ -67,6 +86,78 @@
<h3 class="fw-bold mb-0">{{ holding.size_hectares }} <small class="text-muted fs-6">Hectares</small></h3>
</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>
{% empty %}
@ -81,13 +172,22 @@
<div class="row g-4 text-center mb-5">
<div class="col-6 col-md-3">
<div class="p-3 border rounded">
<small class="text-muted text-uppercase fw-bold d-block mb-1">Crop Output</small>
<span class="h4 fw-bold mb-0 text-success">0.0 t</span>
<small class="text-muted text-uppercase fw-bold d-block mb-1">Crop Area</small>
<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 class="col-6 col-md-3">
<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>
</div>
</div>
@ -109,7 +209,7 @@
<div class="me-3 fs-3 text-secondary"><i class="bi bi-info-circle-fill"></i></div>
<div>
<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>
@ -117,4 +217,4 @@
</div>
</div>
</section>
{% endblock %}
{% endblock %}

View File

@ -6,30 +6,87 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('activityChart').getContext('2d');
const labels = [{% for item in activity_stats %}'{{ item.label }}',{% endfor %}];
const values = [{% for item in activity_stats %}{{ item.value }},{% endfor %}];
// Activity Distribution Chart
const activityCtx = document.getElementById('activityChart').getContext('2d');
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',
data: {
labels: labels,
labels: activityLabels,
datasets: [{
label: 'Agricultural Activity Distribution',
data: values,
backgroundColor: [
'#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'
],
label: 'Agricultural Activity',
data: activityValues,
backgroundColor: ['#003580', '#009543', '#FFD100', '#6c757d', '#dc3545'],
borderWidth: 1
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom',
}
}
plugins: { 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>
<section class="container my-5 pt-4">
<!-- Main KPIs -->
<div class="row g-4 text-center">
<!-- Stats Cards -->
<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">
<h6 class="text-muted text-uppercase mb-2">Total Farmers</h6>
<h2 class="fw-bold mb-0 text-primary">{{ total_farmers }}</h2>
@ -65,7 +122,7 @@
</div>
</div>
<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">
<h6 class="text-muted text-uppercase mb-2">Active Holdings</h6>
<h2 class="fw-bold mb-0 text-success">{{ total_holdings }}</h2>
@ -73,82 +130,122 @@
</div>
</div>
<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">
<h6 class="text-muted text-uppercase mb-2">National Regions</h6>
<h2 class="fw-bold mb-0">14</h2>
<h6 class="text-muted text-uppercase mb-2">Total Parcels</h6>
<h2 class="fw-bold mb-0">{{ land_stats.total_parcels|default:0 }}</h2>
</div>
</div>
</div>
<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">
<h6 class="text-muted text-uppercase mb-2">System Status</h6>
<h2 class="fw-bold mb-0 text-muted">ONLINE</h2>
<h6 class="text-muted text-uppercase mb-2">Total Crop Area</h6>
<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>
<!-- Charts Row 1: Activity & Regions -->
<div class="row mt-5">
<!-- Activity Chart -->
<div class="col-lg-5 mb-4 mb-lg-0">
<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">Activity Distribution</h5>
</div>
<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>
</div>
</div>
</div>
</div>
<!-- Region Distribution Table -->
<div class="col-lg-7">
<div class="col-lg-7 mb-4">
<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">
<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 class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Region</th>
<th>Registered Farmers</th>
<th>Status</th>
<th>Count</th>
<th>Progress</th>
</tr>
</thead>
<tbody>
{% for stat in region_stats %}
{% for stat in region_stats|slice:":5" %}
<tr>
<td class="fw-bold">{{ stat.name }}</td>
<td>{{ stat.count }}</td>
<td><span class="badge bg-success-subtle text-success">Verified</span></td>
<td style="width: 30%">
{% with stat.count|default:0 as cnt %}
<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>
<td style="width: 50%">
<div class="progress" style="height: 6px;">
<div class="progress-bar bg-primary" role="progressbar" style="width: {{ stat.count }}%;"></div>
</div>
{% endwith %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="4" class="text-center py-4 text-muted">No data available yet.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="mt-3">
<a href="{% url 'export_report' %}" class="btn btn-outline-success w-100 fw-bold">Download Full Regional Performance Report (CSV)</a>
</div>
</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>

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 import messages
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
import csv
from django.db.models import Sum, Count
def dashboard(request):
"""Public National Dashboard Overview with Analytics"""
@ -24,13 +28,44 @@ def dashboard(request):
count = AgriculturalHolding.objects.filter(primary_activity=code).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 = {
"project_name": "NAIMS - Namibia",
"total_farmers": total_farmers,
"total_holdings": total_holdings,
"region_stats": sorted(region_stats, key=lambda x: x['count'], reverse=True),
"activity_stats": activity_stats,
"crop_distribution": crop_distribution,
"livestock_distribution": livestock_distribution,
"land_tenure_distribution": land_tenure_distribution,
"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)
@ -81,21 +116,42 @@ def farmer_detail(request, pk):
return render(request, "core/farmer_detail.html", {"farmer": farmer})
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['Content-Disposition'] = 'attachment; filename="naims_agricultural_holdings_report.csv"'
response['Content-Disposition'] = 'attachment; filename="naims_comprehensive_agricultural_report.csv"'
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()
for h in holdings:
farmers = Farmer.objects.all().select_related('constituency__region')
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([
h.farmer.name,
h.farmer.constituency.region.name,
h.farmer.constituency.name,
h.get_primary_activity_display(),
h.size_hectares
f.name,
f.id_number,
f.constituency.region.name,
f.constituency.name,
", ".join([h.get_primary_activity_display() for h in holdings]),
total_size,
crops,
livestock,
forestry,
fisheries,
land
])
return response