Autosave: 20260223-202031
This commit is contained in:
parent
415a23fcaf
commit
f416d7d3a7
Binary file not shown.
@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||
@ -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',
|
||||
},
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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"
|
||||
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
Loading…
x
Reference in New Issue
Block a user