Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Your new fix here.


## [1.2.0] - 2025-08-07

### Added
- **CRUD Management for Absence Codes:**
- Implemented a new "Settings" page at `/settings/absences` for full CRUD (Create, Read, Update, Delete) management of absence codes.
- Created a set of RESTful API endpoints under `/settings/api/absence-codes` to support the new management interface.
- Added a comprehensive test suite (`tests/test_settings.py`) with 100% code coverage for the new backend routes and logic.

### Changed
- The "Manual Entry" dropdown is now dynamically populated with the user-managed absence codes from the database, replacing the previous hardcoded list.
- The "Calendar Log" view now fetches absence codes from the new centralized API endpoint, ensuring consistency across the application.

### Fixed
- Prevented the deletion of absence codes that are currently in use in any `ScheduleEntry`, ensuring data integrity.


## [1.1.1] - 2025-08-07

### Changed
Expand Down
2 changes: 2 additions & 0 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from app.routes.main import main
from app.routes.manual_entry import manual_entry
from app.routes.monthly_log import monthly_log_bp
from app.routes.settings import settings_bp
from app.routes.time_log import time_log
from app.routes.time_summary import time_summary

Expand All @@ -21,5 +22,6 @@ def create_app(config_object):
app.register_blueprint(time_summary)
app.register_blueprint(time_log)
app.register_blueprint(monthly_log_bp)
app.register_blueprint(settings_bp)

return app
19 changes: 3 additions & 16 deletions app/routes/manual_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
@manual_entry.route("/entry", methods=["GET"])
def show_entry_form():
employees = Employee.query.all()
absence_codes = AbsenceCode.query.all()
# Fetch codes dynamically from the database
absence_codes = AbsenceCode.query.order_by(AbsenceCode.code).all()
return render_template(
"manual_entry.html", employees=employees, absence_codes=absence_codes
)
Expand All @@ -33,47 +34,34 @@ def save_entry():
if not validate_date(date_str):
return jsonify({"error": "Invalid date format"}), 400

# Skip validation if it's an absence day
absence_code = data.get("absence_code")

# For work days, validate entries
if absence_code is None:
entries = data.get("entries")
if entries is None:
return jsonify({"error": "Entries are required for work day"}), 400

# Check if entries is empty
if not entries:
return jsonify({"error": "No time entries provided for work day"}), 400

# Validate the entries
is_valid, error = validate_entries(entries)
if not is_valid:
return jsonify({"error": error}), 400

# Convert date string to date object
entry_date = datetime.strptime(date_str, "%Y-%m-%d").date()

# Get employee_id safely
employee_id = data.get("employee_id")
if employee_id is None:
return jsonify({"error": "Employee ID is required"}), 400

# Check if an entry already exists for this date and employee
existing_entry = ScheduleEntry.query.filter_by(
employee_id=employee_id, date=entry_date
).first()

# Get entries safely
entries = data.get("entries", [])

if existing_entry:
# Update existing entry
existing_entry.entries = [] if absence_code else entries
existing_entry.absence_code = absence_code
db.session.commit()
else:
# Create new entry
schedule_entry = ScheduleEntry(
employee_id=employee_id,
date=entry_date,
Expand All @@ -83,7 +71,6 @@ def save_entry():
db.session.add(schedule_entry)
db.session.commit()

# Calculate hours if not an absence
if absence_code is None:
hours = calculate_daily_hours(entries)
return jsonify({"status": "success", "hours": hours})
Expand All @@ -98,7 +85,7 @@ def get_entry(date):

entry = ScheduleEntry.query.filter_by(
date=datetime.strptime(date, "%Y-%m-%d").date(),
employee_id=1, # Default employee ID until login is implemented
employee_id=1,
).first()

if entry:
Expand Down
10 changes: 0 additions & 10 deletions app/routes/monthly_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ def view_monthly_log():
return render_template("monthly_log.html")


@monthly_log_bp.route("/api/absence-codes", methods=["GET"])
def get_absence_codes():
"""Returns a list of available absence codes."""
try:
codes = AbsenceCode.query.all()
return jsonify([code.code for code in codes])
except Exception as e:
return jsonify({"error": str(e)}), 500


@monthly_log_bp.route("/api/<int:year>/<int:month>", methods=["GET"])
def get_monthly_log_data(year, month):
"""
Expand Down
101 changes: 101 additions & 0 deletions app/routes/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from flask import Blueprint, jsonify, render_template, request

from app.db.database import db
from app.models.models import AbsenceCode, ScheduleEntry

settings_bp = Blueprint("settings", __name__, url_prefix="/settings")


@settings_bp.route("/absences", methods=["GET"])
def manage_absences_page():
"""Renders the absence code management page."""
return render_template("settings_absences.html")


# --- API Endpoints ---


@settings_bp.route("/api/absence-codes", methods=["GET"])
def get_absence_codes():
"""Returns a list of all available absence codes."""
try:
codes = AbsenceCode.query.order_by(AbsenceCode.code).all()
return jsonify([{"id": code.id, "code": code.code} for code in codes])
except Exception as e:
return jsonify({"error": str(e)}), 500


@settings_bp.route("/api/absence-codes", methods=["POST"])
def create_absence_code():
"""Creates a new absence code."""
data = request.json
if not data or not data.get("code"):
return jsonify({"error": "Code is required"}), 400

new_code_str = data["code"].strip()
if not new_code_str:
return jsonify({"error": "Code cannot be empty"}), 400

existing_code = AbsenceCode.query.filter_by(code=new_code_str).first()
if existing_code:
return jsonify({"error": "Code already exists"}), 409

try:
new_code = AbsenceCode(code=new_code_str)
db.session.add(new_code)
db.session.commit()
return jsonify({"id": new_code.id, "code": new_code.code}), 201
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500


@settings_bp.route("/api/absence-codes/<int:code_id>", methods=["PUT"])
def update_absence_code(code_id):
"""Updates an existing absence code."""
data = request.json
if not data or not data.get("code"):
return jsonify({"error": "Code is required"}), 400

new_code_str = data["code"].strip()
if not new_code_str:
return jsonify({"error": "Code cannot be empty"}), 400

code_to_update = db.session.get(AbsenceCode, code_id)
if not code_to_update:
return jsonify({"error": "Code not found"}), 404

existing_code = AbsenceCode.query.filter(
AbsenceCode.id != code_id, AbsenceCode.code == new_code_str
).first()
if existing_code:
return jsonify({"error": "Another code with this name already exists"}), 409

try:
code_to_update.code = new_code_str
db.session.commit()
return jsonify({"id": code_to_update.id, "code": code_to_update.code})
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500


@settings_bp.route("/api/absence-codes/<int:code_id>", methods=["DELETE"])
def delete_absence_code(code_id):
"""Deletes an absence code."""
code_to_delete = db.session.get(AbsenceCode, code_id)
if not code_to_delete:
return jsonify({"error": "Code not found"}), 404

# Manually check if the code is in use before attempting to delete.
is_in_use = ScheduleEntry.query.filter_by(absence_code=code_to_delete.code).first()
if is_in_use:
return jsonify({"error": "Cannot delete code, it is currently in use."}), 409

try:
db.session.delete(code_to_delete)
db.session.commit()
return jsonify({"status": "success"}), 200
except Exception as e:
db.session.rollback()
return jsonify({"error": str(e)}), 500
21 changes: 11 additions & 10 deletions app/static/js/monthly_log.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,33 +18,34 @@ document.addEventListener('DOMContentLoaded', function() {
let currentDate = new Date();
let selectedDates = [];
let isMouseDown = false;
let absenceCodes = [];

async function fetchAbsenceCodes() {
try {
const response = await fetch('/monthly-log/api/absence-codes');
// Point to the new, centralized API endpoint
const response = await fetch('/settings/api/absence-codes');
if (!response.ok) throw new Error('Failed to fetch absence codes');
absenceCodes = await response.json();
// The new API returns a list of objects, so we return that directly
return await response.json();
} catch (error) {
console.error(error);
return [];
}
}

function populateDayTypeSelect() {
function populateDayTypeSelect(absenceCodes) {
dayTypeSelect.innerHTML = '';
// Add special "Default" option first
dayTypeSelect.add(new Option("(Revert to Default)", "DEFAULT"));
dayTypeSelect.add(new Option("Work Day", "Work Day"));

if (absenceCodes && absenceCodes.length > 0) {
absenceCodes.forEach(code => {
dayTypeSelect.add(new Option(code.replace(/_/g, ' '), code));
// Adjust to handle the new object format {id, code}
absenceCodes.forEach(item => {
dayTypeSelect.add(new Option(item.code.replace(/_/g, ' '), item.code));
});
}
}

// ... (el resto de las funciones de JS no cambian)

function populateSelectors() {
const currentYear = new Date().getFullYear();
const startYear = currentYear - 5;
Expand Down Expand Up @@ -192,8 +193,8 @@ document.addEventListener('DOMContentLoaded', function() {
async function init() {
populateSelectors();
updateSelectors();
await fetchAbsenceCodes();
populateDayTypeSelect();
const absenceCodes = await fetchAbsenceCodes();
populateDayTypeSelect(absenceCodes);
await renderCalendar();
}

Expand Down
Loading