mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-20 19:39:59 -06:00
- Clients: add model, routes, and templates
- app/models/client.py
- app/routes/clients.py
- templates/clients/{create,edit,list,view}.html
- docs/CLIENT_MANAGEMENT_README.md
- Database: add enhanced init/verify scripts, migrations, and docs
- docker/{init-database-enhanced.py,start-enhanced.py,verify-database.py}
- docs/ENHANCED_DATABASE_STARTUP.md
- migrations/{add_analytics_column.sql,add_analytics_setting.py,migrate_to_client_model.py}
- Scripts: add version manager and docker network test helpers
- scripts/version-manager.{bat,ps1,py,sh}
- scripts/test-docker-network.{bat,sh}
- docs/VERSION_MANAGEMENT.md
- UI: tweak base stylesheet
- app/static/base.css
- Tests: add client system test
- test_client_system.py
217 lines
10 KiB
HTML
217 lines
10 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Log Time - {{ app_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8 col-md-10">
|
|
<div class="card">
|
|
<div class="card-header d-flex align-items-center">
|
|
<i class="fas fa-plus me-2 text-primary"></i>
|
|
<h5 class="mb-0">Log Time Manually</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="POST" action="{{ url_for('timer.manual_entry') }}">
|
|
<div class="mb-4">
|
|
<label for="project_id" class="form-label fw-semibold">
|
|
<i class="fas fa-project-diagram me-1"></i>Project *
|
|
</label>
|
|
<select class="form-select" id="project_id" name="project_id" required>
|
|
<option value="">Select a project...</option>
|
|
{% set selected_project_id = (request.form.get('project_id') or '')|int %}
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}" {% if project.id == selected_project_id %}selected{% endif %}>{{ project.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="task_id" class="form-label fw-semibold">
|
|
<i class="fas fa-tasks me-1"></i>Task (optional)
|
|
</label>
|
|
{% set preselected_task_id = request.form.get('task_id') or request.args.get('task_id') %}
|
|
<select class="form-select" id="task_id" name="task_id" data-selected-task-id="{{ preselected_task_id or '' }}" disabled>
|
|
<option value="">No task</option>
|
|
</select>
|
|
<div class="form-text">Tasks will be loaded for the selected project.</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-6">
|
|
<div class="mb-4">
|
|
<label class="form-label fw-semibold">
|
|
<i class="fas fa-play me-1"></i>Start *
|
|
</label>
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<input type="date" class="form-control" name="start_date" id="start_date" required value="{{ request.form.get('start_date','') }}">
|
|
</div>
|
|
<div class="col-6">
|
|
<input type="time" class="form-control" name="start_time" id="start_time" required value="{{ request.form.get('start_time','') }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<div class="mb-4">
|
|
<label class="form-label fw-semibold">
|
|
<i class="fas fa-stop me-1"></i>End *
|
|
</label>
|
|
<div class="row g-2">
|
|
<div class="col-6">
|
|
<input type="date" class="form-control" name="end_date" id="end_date" required value="{{ request.form.get('end_date','') }}">
|
|
</div>
|
|
<div class="col-6">
|
|
<input type="time" class="form-control" name="end_time" id="end_time" required value="{{ request.form.get('end_time','') }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label for="notes" class="form-label fw-semibold">
|
|
<i class="fas fa-sticky-note me-1"></i>Notes
|
|
</label>
|
|
<textarea class="form-control" id="notes" name="notes" rows="3" placeholder="What did you work on?">{{ request.form.get('notes','') }}</textarea>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-8">
|
|
<div class="mb-4">
|
|
<label for="tags" class="form-label fw-semibold">
|
|
<i class="fas fa-tags me-1"></i>Tags
|
|
</label>
|
|
<input type="text" class="form-control" id="tags" name="tags" placeholder="tag1, tag2, tag3" value="{{ request.form.get('tags','') }}">
|
|
<div class="form-text">Separate tags with commas</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-12 col-md-4 d-flex align-items-center">
|
|
<div class="form-check form-switch mt-4 w-100">
|
|
<input class="form-check-input" type="checkbox" id="billable" name="billable" {% if request.form.get('billable') %}checked{% else %}checked{% endif %}>
|
|
<label class="form-check-label fw-semibold" for="billable">
|
|
<i class="fas fa-dollar-sign me-1"></i>Billable
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex justify-content-between flex-column flex-md-row">
|
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary mb-2 mb-md-0">
|
|
<i class="fas fa-arrow-left me-1"></i>Back
|
|
</a>
|
|
<button type="submit" class="btn btn-primary">
|
|
<i class="fas fa-save me-2"></i>Save Entry
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Set default dates to today
|
|
const today = new Date().toISOString().split('T')[0];
|
|
const now = new Date().toTimeString().slice(0, 5);
|
|
|
|
if (!document.getElementById('start_date').value) {
|
|
document.getElementById('start_date').value = today;
|
|
}
|
|
if (!document.getElementById('end_date').value) {
|
|
document.getElementById('end_date').value = today;
|
|
}
|
|
if (!document.getElementById('start_time').value) {
|
|
document.getElementById('start_time').value = now;
|
|
}
|
|
if (!document.getElementById('end_time').value) {
|
|
document.getElementById('end_time').value = now;
|
|
}
|
|
|
|
// Mobile-specific improvements
|
|
if (window.innerWidth <= 768) {
|
|
// Add mobile-specific classes
|
|
const form = document.querySelector('form');
|
|
form.classList.add('mobile-form');
|
|
|
|
// Improve touch targets
|
|
const inputs = document.querySelectorAll('.form-control, .form-select');
|
|
inputs.forEach(input => {
|
|
input.classList.add('touch-target');
|
|
});
|
|
|
|
// Improve buttons
|
|
const buttons = document.querySelectorAll('.btn');
|
|
buttons.forEach(btn => {
|
|
btn.classList.add('touch-target');
|
|
});
|
|
}
|
|
|
|
// Handle mobile viewport changes
|
|
window.addEventListener('resize', function() {
|
|
if (window.innerWidth <= 768) {
|
|
document.body.classList.add('mobile-view');
|
|
} else {
|
|
document.body.classList.remove('mobile-view');
|
|
}
|
|
});
|
|
|
|
// Dynamic task loading based on project selection
|
|
const projectSelect = document.getElementById('project_id');
|
|
const taskSelect = document.getElementById('task_id');
|
|
|
|
async function loadTasksForProject(projectId) {
|
|
if (!projectId) {
|
|
taskSelect.innerHTML = '<option value="">No task</option>';
|
|
taskSelect.disabled = true;
|
|
return;
|
|
}
|
|
try {
|
|
const resp = await fetch(`/api/tasks?project_id=${projectId}`);
|
|
if (!resp.ok) throw new Error('Failed to load tasks');
|
|
const data = await resp.json();
|
|
const tasks = Array.isArray(data.tasks) ? data.tasks : [];
|
|
taskSelect.innerHTML = '<option value="">No task</option>';
|
|
tasks.forEach(t => {
|
|
const opt = document.createElement('option');
|
|
opt.value = String(t.id);
|
|
opt.textContent = t.name;
|
|
taskSelect.appendChild(opt);
|
|
});
|
|
// Preselect if provided
|
|
const preId = taskSelect.getAttribute('data-selected-task-id');
|
|
if (preId) {
|
|
const found = Array.from(taskSelect.options).some(o => o.value === preId);
|
|
if (found) taskSelect.value = preId;
|
|
// Clear after first use
|
|
taskSelect.setAttribute('data-selected-task-id', '');
|
|
}
|
|
taskSelect.disabled = false;
|
|
} catch (e) {
|
|
// On error, keep disabled
|
|
taskSelect.innerHTML = '<option value="">No task</option>';
|
|
taskSelect.disabled = true;
|
|
}
|
|
}
|
|
|
|
// Initial load if project is already selected (from query/form)
|
|
if (projectSelect && projectSelect.value) {
|
|
loadTasksForProject(projectSelect.value);
|
|
}
|
|
|
|
// Reload tasks when project changes
|
|
projectSelect.addEventListener('change', function() {
|
|
// Clear any previous selection
|
|
taskSelect.value = '';
|
|
taskSelect.setAttribute('data-selected-task-id', '');
|
|
loadTasksForProject(this.value);
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|
|
|
|
|