11 Commits

Author SHA1 Message Date
e41eea8527 fix hb inline for admin 2025-12-05 18:07:49 +01:00
369c3b5e19 deploy script 2025-12-05 18:00:27 +01:00
aeb3aa30ce Show Hour building only at ATPL or DL (volo) students 2025-12-05 17:58:51 +01:00
84cf41535c No --- in notes 2025-12-04 15:07:23 +01:00
cdf7e7c677 Show only mission correct for student phase, add only one preference per week 2025-12-04 14:51:52 +01:00
b8f4331d3b Improved xlsx formatting 2025-12-04 13:05:34 +01:00
303359c921 refix merge 2025-12-03 10:27:54 +01:00
ec8373877b fix row merge 2025-12-03 10:21:50 +01:00
e7e47152ed fixed excel formatting 2025-12-03 10:10:00 +01:00
1eb11f33fc fix phone export 2025-12-03 09:23:27 +01:00
e417268991 Merge pull request 'instructor-class' (#3) from instructor-class into flightslot
Reviewed-on: #3
2025-12-02 12:33:41 +01:00
11 changed files with 170 additions and 76 deletions

5
cntmanage/docker/deploy.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/bash
git pull
./build
docker compose up -d

View File

@@ -5,6 +5,7 @@ from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter from openpyxl.utils import get_column_letter
from ..models.courses import CourseTypes
from ..models.missions import Training from ..models.missions import Training
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuildingLegStop, HourBuildingLegBase
@@ -12,6 +13,19 @@ from ..models.hourbuildings import HourBuilding, HourBuildingLegFlight, HourBuil
from datetime import date, datetime from datetime import date, datetime
from typing import List from typing import List
# Enable cell merging for equal mission
MERGE: bool = False
PALETTE : List[str] = [
"#E6F2FF", # azzurro chiarissimo
"#E5FBF8", # verde acqua molto chiaro
"#ECFBE1", # verde chiarissimo
"#FFFBD1", # giallo molto chiaro
"#FFF1D6", # giallo-arancio molto chiaro
"#FFE3DD", # rosa pesca molto chiaro
"#F3E6FA", # lilla chiarissimo
]
def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse: def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse:
if not queryset.first(): if not queryset.first():
@@ -57,7 +71,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
center = Alignment(horizontal="center", vertical="center", wrapText=True) center = Alignment(horizontal="center", vertical="center", wrapText=True)
# Cell styles # Cell styles
border_thick: Side = Side(style='thick', color='000000') border_thick: Side = Side(style='medium', color='000000')
border_thin: Side = Side(style='thin', color='000000', border_style='dashed') border_thin: Side = Side(style='thin', color='000000', border_style='dashed')
border_bottom: Border = Border(bottom=border_thick) border_bottom: Border = Border(bottom=border_thick)
border_bottom_thin: Border = Border(bottom=border_thin) border_bottom_thin: Border = Border(bottom=border_thin)
@@ -78,74 +92,78 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Each of this iterations fills the table for a student # Each of this iterations fills the table for a student
row: int = 2 row: int = 2
row_offset: int = 0 row_offset: int = 0
for i, q in enumerate(queryset.order_by("week", "student__surname", "student__name", "student__course"), start=1): for i, q in enumerate(queryset.order_by("-week", "student__surname", "student__name", "student__course"), start=1):
student_data: List[str] student_data: List[str]
student_phone: str = q.student.phone if q.student.phone else "" student_phone: str = str(q.student.phone) if q.student.phone else ""
student_email: str = q.student.email student_email: str = q.student.email
student_course_type: str student_course_type: str
student_course_number: str student_course_number: str
student_course_ac: str student_course_ac: str = f"({'/'.join(t.type for t in q.student.aircrafts.distinct("type").all())})"
if q.student.course: if q.student.course:
student_course_type = q.student.course.ctype student_course_type = q.student.course.ctype
student_course_number = str(q.student.course.cnumber) student_course_number = str(q.student.course.cnumber)
student_course_ac = " / ".join(t.type for t in q.student.aircrafts.distinct("type").all())
student_data = [ student_data = [
f"{q.student.surname} {q.student.name}\n{student_course_ac}", "\n".join([f"{q.student.surname} {q.student.name}", student_course_ac]),
f"{student_course_type}-{student_course_number}" f"{student_course_type}-{student_course_number}"
] ]
else: else:
student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"] student_data = [f"{q.student.surname} {q.student.name}", f"No Course Assigned"]
# Fill Training mission rows # Fill Training mission rows
mission_name: str mission_name: List[str]
mission_days: List[str] mission_days: List[str]
mission_notes: str mission_notes: str
mission_data: List[List[str]] = [] mission_data: List[List[str]] = []
for t in Training.objects.filter(weekpref = q.id): for t in Training.objects.filter(weekpref = q.id):
if not t.mission: if not t.mission:
raise Exception("No Training Mission Assigned") raise Exception("No Training Mission Assigned")
mission_name = f"{t.mission.mtype}-{t.mission.mnum}" mission_notes = t.notes.strip().capitalize() if t.notes else ""
mission_name = [f"{t.mission.mtype}-{t.mission.mnum}"]
if q.student.course and q.student.course.ctype == CourseTypes.PPL: # add course aircraft only for PPL students
mission_name.append(student_course_ac)
mission_name_joined: str = "\n".join(mission_name)
mission_days = [ mission_days = [
mission_name if t.monday else "", mission_name_joined if t.monday else "",
mission_name if t.tuesday else "", mission_name_joined if t.tuesday else "",
mission_name if t.wednesday else "", mission_name_joined if t.wednesday else "",
mission_name if t.thursday else "", mission_name_joined if t.thursday else "",
mission_name if t.friday else "", mission_name_joined if t.friday else "",
mission_name if t.saturday else "", mission_name_joined if t.saturday else "",
mission_name if t.sunday else "" mission_name_joined if t.sunday else ""
] ]
mission_notes = t.notes if t.notes else "--"
mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ]) mission_data.append([str(q.week), *student_data, *mission_days, mission_notes, student_phone, student_email, ])
# Fill HourBuilding rows # Fill HourBuilding rows
hb_name: str hb_name: List[str]
hb_days: List[str] hb_days: List[str]
hb_data: List[List[str]] = [] hb_data: List[List[str]] = []
for h in HourBuilding.objects.filter(weekpref = q.id): for h in HourBuilding.objects.filter(weekpref = q.id):
hb_name = f"HB - {h.aircraft}\nVedi Note ->" hb_name = ["HB", f"({h.aircraft})"]
hb_days = [
hb_name if h.monday else "",
hb_name if h.tuesday else "",
hb_name if h.wednesday else "",
hb_name if h.thursday else "",
hb_name if h.friday else "",
hb_name if h.saturday else "",
hb_name if h.sunday else ""
]
hb_notes: List[str] = [f"{h.notes}", "---"] if h.notes else []
hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id) hb_legs_all = HourBuildingLegBase.objects.filter(hb_id = h.id)
for hh in hb_legs_all: for hh in hb_legs_all:
time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes time_str: str = ':'.join(str(hh.time).split(':')[:2]) # keep only hours and minutes
if isinstance(hh, HourBuildingLegFlight): if isinstance(hh, HourBuildingLegFlight):
hb_notes.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / PAX: {hh.pax.capitalize()}' if hh.pax else ''}") hb_pax: str | None = " ".join(x.capitalize() for x in hh.pax.split()) if hh.pax else None
hb_name.append(f"{hh.departure} -> {hh.destination} [{time_str}]{f' / Pax: {hb_pax}' if hb_pax else ''}")
elif isinstance(hh, HourBuildingLegStop): elif isinstance(hh, HourBuildingLegStop):
hb_notes.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" ) hb_name.append(f"STOP [{time_str}] {"Refuel" if hh.refuel else ""}" )
hb_data.append([str(q.week), *student_data, *hb_days, "\n".join(hb_notes), str(q.student.phone), q.student.email]) hb_name_joined: str = "\n".join(hb_name)
hb_days = [
hb_name_joined if h.monday else "",
hb_name_joined if h.tuesday else "",
hb_name_joined if h.wednesday else "",
hb_name_joined if h.thursday else "",
hb_name_joined if h.friday else "",
hb_name_joined if h.saturday else "",
hb_name_joined if h.sunday else ""
]
hb_notes: str = h.notes.strip().capitalize() if h.notes else ""
hb_data.append([str(q.week), *student_data, *hb_days, hb_notes, str(q.student.phone), q.student.email])
# Build rows for table # Build rows for table
all_data: List[List[str]] = mission_data + hb_data all_data: List[List[str]] = mission_data + hb_data
student_start: int = row + row_offset student_start: int = row + row_offset
for row_content in all_data: for ri, row_content in enumerate(all_data):
for c, cell_content in enumerate(row_content, start=1): for c, cell_content in enumerate(row_content, start=1):
cell = ws.cell(row = row + row_offset, column = c, value = cell_content) cell = ws.cell(row = row + row_offset, column = c, value = cell_content)
cell.alignment = center cell.alignment = center
@@ -160,10 +178,10 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
elif c > course_index and c <= note_index: elif c > course_index and c <= note_index:
cell.border = border_bottom_thin + border_right_thin cell.border = border_bottom_thin + border_right_thin
# Fill mix cells if the cell is not empty # Fill mix cells if the cell is not empty
if c > course_index and c < note_index: if c > course_index and c <= note_index:
if len(cell_content): if len(cell_content):
cell.fill = PatternFill('solid', fgColor="f0f0f0") cell.fill = PatternFill('solid', fgColor=PALETTE[ri % len(PALETTE)].lstrip("#").lower())
if MERGE:
prev_cell_val: str = row_content[0] prev_cell_val: str = row_content[0]
merge_start: bool = False merge_start: bool = False
merge_col_start: int = 1 merge_col_start: int = 1
@@ -191,8 +209,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin) ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thin)
# And for last column also a vertical border all student high # And for last column also a vertical border all student high
if c == mail_index: if c == mail_index:
for row_content in range(student_start, student_end + 1): ws.cell(row=student_end, column=c).border = Border(bottom=border_thick, right=border_thick)
ws.cell(row=row_content, column=c).border += border_right
# Merge Week, thick border # Merge Week, thick border
ws.cell(row=student_start, column=week_index).border = border_all ws.cell(row=student_start, column=week_index).border = border_all
@@ -209,6 +226,7 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
# Keep the largest column # Keep the largest column
max_len: List[int] = [] max_len: List[int] = []
col_letter: str = "A"
for column_cells in ws.columns: for column_cells in ws.columns:
for cell in column_cells: for cell in column_cells:
cell_lines = str(cell.value).splitlines() cell_lines = str(cell.value).splitlines()
@@ -216,11 +234,9 @@ def export_selected(request: HttpRequest, queryset: QuerySet[WeekPreference]) ->
continue continue
max_len.append(max([len(ll) for ll in cell_lines])) max_len.append(max([len(ll) for ll in cell_lines]))
length: int = max(max_len) length: int = max(max_len)
col_letter: str = "A"
if column_cells[0].column: if column_cells[0].column:
col_letter = get_column_letter(column_cells[0].column) col_letter = get_column_letter(column_cells[0].column)
ws.column_dimensions[col_letter].width = length + 2 ws.column_dimensions[col_letter].width = length + 2
### End of Student Loop ### ### End of Student Loop ###
# Save document in HttpResponse # Save document in HttpResponse

View File

@@ -59,8 +59,8 @@ class FlightSlotStaffSite(AdminSite):
# Register only user visible models # Register only user visible models
flightslot_staff = FlightSlotUserSite(name="staff_site") flightslot_staff = FlightSlotUserSite(name="staff_site")
flightslot_staff.register(Availability, AvailabilityAdmin)
flightslot_staff.register(MissionProfile, MissionProfileAdmin) flightslot_staff.register(MissionProfile, MissionProfileAdmin)
flightslot_staff.register(Availability, AvailabilityAdmin)
flightslot_staff.register(Instructor, InstructorAdmin) flightslot_staff.register(Instructor, InstructorAdmin)
# Get version for debug purposes # Get version for debug purposes
@@ -75,5 +75,5 @@ admin.site.register(Course, CourseAdmin)
admin.site.register(MissionProfile, MissionProfileAdmin) admin.site.register(MissionProfile, MissionProfileAdmin)
admin.site.register(Student, StudentAdmin) admin.site.register(Student, StudentAdmin)
admin.site.register(WeekPreference, WeekPreferenceAdmin) admin.site.register(WeekPreference, WeekPreferenceAdmin)
admin.site.register(Instructor, InstructorAdmin) #admin.site.register(Instructor, InstructorAdmin)
admin.site.register(Availability, AvailabilityAdmin) #admin.site.register(Availability, AvailabilityAdmin)

View File

@@ -2,8 +2,8 @@ import nested_admin
from django import forms from django import forms
from django.db import models from django.db import models
from django.forms import TextInput, Textarea
from django.http import HttpRequest from django.http import HttpRequest
from django.forms import TextInput, Textarea
from durationwidget.widgets import TimeDurationWidget from durationwidget.widgets import TimeDurationWidget

View File

@@ -1,16 +1,18 @@
import nested_admin import nested_admin
from django import forms from django import forms
from django.db import models from django.db.models import CharField, TextField
from django.db.models.query_utils import Q
from django.db.models.fields.related import ForeignKey
from django.forms import TextInput, Textarea from django.forms import TextInput, Textarea
from django.http import HttpRequest from django.http import HttpRequest
from ..models.missions import Training from ..models.courses import Course, CourseTypes
from ..models.students import Student
from ..models.missions import Training, MissionTypes, MissionProfile
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
from ..custom.student_permissions import has_edit_permission from ..custom.student_permissions import has_edit_permission
from datetime import date
class TrainingForm(forms.ModelForm): class TrainingForm(forms.ModelForm):
model=Training model=Training
@@ -23,10 +25,32 @@ class TrainingInLIne(nested_admin.NestedTabularInline):
max_num = 7 max_num = 7
formfield_overrides = { formfield_overrides = {
models.CharField: {'widget': TextInput(attrs={'size':'20'})}, CharField: {'widget': TextInput(attrs={'size':'20'})},
models.TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})}, TextField: {'widget': Textarea(attrs={'rows':4, 'cols':35})},
} }
def formfield_for_foreignkey(self, db_field: ForeignKey, request: HttpRequest, **kwargs):
# modify entries for "mission" field, show only types compatible with student course
if not hasattr(request.user, "student") or not hasattr(request.user.student, "course"):
return super().formfield_for_foreignkey(db_field, request, **kwargs)
course: Course = request.user.student.course
if db_field.name == "mission":
match course.ctype:
case CourseTypes.PPL:
kwargs["queryset"] = MissionProfile.objects.filter(mtype=MissionTypes.PPL)
case CourseTypes.ATPL:
q: Q = Q(mtype=MissionTypes.IR) | \
Q(mtype=MissionTypes.MEP) | \
Q(mtype=MissionTypes.MEP_IR) | \
Q(mtype=MissionTypes.CPL) | \
Q(mtype=MissionTypes.CHK)
kwargs["queryset"] = MissionProfile.objects.filter(q).order_by("id")
case CourseTypes.FI:
kwargs["queryset"] = MissionProfile.objects.filter(mtype=MissionTypes.FI).order_by("mnum")
case _:
pass
return super().formfield_for_foreignkey(db_field, request, **kwargs)
# If user is a student deny edit permission for week past the current one # If user is a student deny edit permission for week past the current one
def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: def has_change_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return has_edit_permission(request=request, obj=obj) return has_edit_permission(request=request, obj=obj)

View File

@@ -7,6 +7,7 @@ from django.contrib import admin, messages
from django.utils.translation import ngettext from django.utils.translation import ngettext
from django.utils.safestring import SafeText from django.utils.safestring import SafeText
from ..models.courses import CourseTypes
from ..models.students import Student from ..models.students import Student
from ..models.missions import Training from ..models.missions import Training
from ..models.weekpref import WeekPreference from ..models.weekpref import WeekPreference
@@ -15,18 +16,17 @@ from .training_adm import TrainingInLIne
from .hourbuilding_adm import HourBuildingInLine from .hourbuilding_adm import HourBuildingInLine
from ..custom.colortag import course_color from ..custom.colortag import course_color
from ..custom.student_permissions import has_edit_permission from ..custom.student_permissions import has_edit_permission, has_week_add_permission
from ..actions.exportweek import export_selected from ..actions.exportweek import export_selected
from datetime import date from datetime import date
from typing import Dict, List, Any from typing import Dict, List, Any
class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin): class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
inlines = (TrainingInLIne, HourBuildingInLine, ) list_display = ("week", "student__surname", "student__name", "student__course", "course_color", "student_brief_mix", )
list_display = ("week", "student__surname","student__name", "student__course", "course_color", "student_brief_mix",) list_filter = ("week", "student__course", )
list_filter = ("week", "student__course",) search_fields = ("student__surname","student__name", )
search_fields = ("student__surname","student__name",) actions = ("export", )
actions = ("export",)
@admin.action(description="Export Selected Preferences") @admin.action(description="Export Selected Preferences")
def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None: def export(self, request: HttpRequest, queryset: QuerySet[WeekPreference]) -> HttpResponse | None:
@@ -54,6 +54,24 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
return [] return []
return list_filter return list_filter
# Get available mission or HB depending on student course
def get_inline_instances(self, request: HttpRequest, obj: WeekPreference | None = None):
if hasattr(request.user, "student"):
student: Student = request.user.student
# Only ATPL students are able to book HourBuilding Missions
if student.course and student.course.ctype in (CourseTypes.ATPL, CourseTypes.DISTANCE):
return (
TrainingInLIne(self.model, self.admin_site),
HourBuildingInLine(self.model, self.admin_site),
)
# All other courses have only training
return (TrainingInLIne(self.model, self.admin_site), )
else:
return (
TrainingInLIne(self.model, self.admin_site),
HourBuildingInLine(self.model, self.admin_site),
)
# If a user is registered as student do not show actions # If a user is registered as student do not show actions
def get_actions(self, request: HttpRequest) -> Dict[str, Any]: def get_actions(self, request: HttpRequest) -> Dict[str, Any]:
actions = super().get_actions(request) actions = super().get_actions(request)
@@ -93,7 +111,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
# If user is a student deny edit permission for week past the current one # If user is a student deny edit permission for week past the current one
def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool: def has_add_permission(self, request: HttpRequest, obj: WeekPreference | None = None) -> bool:
return not obj and self.has_change_permission(request, obj) return has_week_add_permission(request=request) and has_edit_permission(request=request, obj=obj)
# If user is a student deny edit permission for week past the current one # If user is a student deny edit permission for week past the current one
def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool: def has_delete_permission(self, request: HttpRequest, obj: WeekPreference | None = None)-> bool:
@@ -110,7 +128,7 @@ class WeekPreferenceAdmin(nested_admin.NestedPolymorphicModelAdmin):
extra_context["show_delete"] = False extra_context["show_delete"] = False
return super().changeform_view(request, object_id, form_url, extra_context) return super().changeform_view(request, object_id, form_url, extra_context)
def save_model(self, request: HttpRequest, obj, form: Form, change: bool): def save_model(self, request: HttpRequest, obj: WeekPreference, form: Form, change: bool):
# Imposta automaticamente lo studente se non è già valorizzato # Imposta automaticamente lo studente se non è già valorizzato
if hasattr(request.user, "student") and not obj.student_id: if hasattr(request.user, "student") and not obj.student_id:
obj.student = request.user.student obj.student = request.user.student

View File

@@ -5,13 +5,17 @@ from ..models.weekpref import WeekPreference
from datetime import date from datetime import date
def has_week_add_permission(request: HttpRequest):
if hasattr(request.user, 'student'):
student: Student = request.user.student
current_week: int = date.today().isocalendar().week
return student.active and not WeekPreference.objects.filter(student_id=student.id, week=current_week).count()
return True
# allow add, modify, delete depending on a set of requirements # allow add, modify, delete depending on a set of requirements
def has_edit_permission(request: HttpRequest, obj: WeekPreference | None = None) -> bool: def has_edit_permission(request: HttpRequest, obj: WeekPreference | None = None) -> bool:
if hasattr(request.user, 'student'): if hasattr(request.user, 'student'):
student: Student = request.user.student
if not student.active:
return False
current_week: int = date.today().isocalendar().week current_week: int = date.today().isocalendar().week
if obj and (current_week > obj.week or not student.active): if obj and current_week > obj.week:
return False return False
return True return True

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.2.8 on 2025-12-05 16:57
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('flightslot', '0028_alter_availability_options'),
]
operations = [
migrations.AlterField(
model_name='course',
name='ctype',
field=models.CharField(choices=[('FI', 'FI'), ('PPL', 'PPL'), ('ATPL', 'ATPL'), ('DL', 'DISTANCE'), ('DL_VOLO', 'DISTANCE_VOLO'), ('OTHER', 'OTHER')], verbose_name='Course Type'),
),
]

View File

@@ -8,6 +8,7 @@ class CourseTypes(models.TextChoices):
PPL = "PPL", _("PPL") PPL = "PPL", _("PPL")
ATPL = "ATPL", _("ATPL") ATPL = "ATPL", _("ATPL")
DISTANCE = "DL", _("DISTANCE") DISTANCE = "DL", _("DISTANCE")
DISTANCE_VOLO = "DL_VOLO", _("DISTANCE_VOLO")
OTHER = "OTHER",_("OTHER") OTHER = "OTHER",_("OTHER")
class Course(models.Model): class Course(models.Model):

View File

@@ -117,6 +117,8 @@ class HourBuildingLegFlight(HourBuildingLegBase):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.departure = self.departure.upper().strip() self.departure = self.departure.upper().strip()
self.destination = self.destination.upper().strip() self.destination = self.destination.upper().strip()
if self.pax:
self.pax = " ".join(c.capitalize() for c in self.pax.split())
super().save(*args, **kwargs) super().save(*args, **kwargs)
def __str__(self): def __str__(self):

6
note.txt Normal file
View File

@@ -0,0 +1,6 @@
OK leg delle hb all'interno del riquadro per i giorni
OK aereo assegnato allo studente di fianco al numero della missione per PPL, invece per CPL e IR e HB il tipo va di fiaco al nome dello studente
OK le missioni ripetute su piu' giorni hanno una cella per giorno (non unite)
lo studente vede solo le missioni della sua fase PPL->PPL ATPL-> tutto
OK ogni richiesta ha un colore diverso che cicla con delle tinte pastello