Files
TimeTracker/templates/projects/create.html
Dries Peeters 69f9f1140d feat(i18n): add translations, locale switcher, and user language preference
- Integrate Flask-Babel and i18n utilities; initialize in app factory
- Add `preferred_language` to `User` with Alembic migration (011_add_user_preferred_language)
- Add `babel.cfg` and `scripts/extract_translations.py`
- Add `translations/` for en, de, fr, it, nl, fi
- Update templates to use `_()` and add language picker in navbar/profile
- Respect locale in routes and context processors; persist user preference
- Update requirements and Docker/Docker entrypoint for Babel/gettext support
- Minor copy and style adjustments across pages

Migration: run `alembic upgrade head`
2025-09-11 23:08:41 +02:00

462 lines
20 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ _('Create Project') }} - {{ app_name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<!-- Header -->
<div class="row mb-4">
<div class="col-12">
<div class="card mobile-card">
<div class="card-body py-4">
<div class="d-flex align-items-center">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-3" style="width: 48px; height: 48px;">
<i class="fas fa-project-diagram text-primary fa-lg"></i>
</div>
<div>
<h1 class="h2 mb-1">{{ _('Create New Project') }}</h1>
<p class="text-muted mb-0">{{ _('Set up a new project to organize your work and track time effectively') }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Create Project Form -->
<div class="row">
<div class="col-lg-8">
<div class="card mobile-card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-edit me-2 text-primary"></i>{{ _('Project Information') }}
</h6>
</div>
<div class="card-body">
<form method="POST" action="{{ url_for('projects.create_project') }}" novalidate id="createProjectForm">
<!-- Project Name and Client -->
<div class="row mb-4">
<div class="col-md-8">
<div class="mb-3">
<label for="name" class="form-label fw-semibold">
<i class="fas fa-tag me-2 text-primary"></i>{{ _('Project Name') }} <span class="text-danger">*</span>
</label>
<input type="text" class="form-control form-control-lg" id="name" name="name" required
value="{{ request.form.get('name','') }}" placeholder="{{ _('Enter a descriptive project name') }}">
<small class="form-text text-muted">{{ _('Choose a clear, descriptive name that explains the project scope') }}</small>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="client_id" class="form-label fw-semibold">
<i class="fas fa-user me-2 text-info"></i>{{ _('Client') }} <span class="text-danger">*</span>
</label>
<select class="form-select form-select-lg" id="client_id" name="client_id" required>
<option value="">{{ _('Select a client...') }}</option>
{% for client in clients %}
<option value="{{ client.id }}"
{% if request.form.get('client_id') == client.id|string %}selected{% endif %}
data-default-rate="{{ client.default_hourly_rate or '' }}">
{{ client.name }}
</option>
{% endfor %}
</select>
<div class="form-text">
<a href="{{ url_for('clients.create_client') }}" class="text-decoration-none">
<i class="fas fa-plus"></i> {{ _('Create new client') }}
</a>
</div>
</div>
</div>
</div>
<!-- Description -->
<div class="mb-4">
<label for="description" class="form-label fw-semibold">
<i class="fas fa-align-left me-2 text-primary"></i>{{ _('Description') }}
</label>
<textarea class="form-control" id="description" name="description" rows="4"
placeholder="{{ _('Provide detailed information about the project, objectives, and deliverables...') }}">{{ request.form.get('description','') }}</textarea>
<small class="form-text text-muted">{{ _('Optional: Add context, objectives, or specific requirements for the project') }}</small>
</div>
<!-- Billing Settings -->
<div class="row mb-4">
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="billable" name="billable" {% if request.form.get('billable') %}checked{% endif %}>
<label class="form-check-label fw-semibold" for="billable">
<i class="fas fa-dollar-sign me-2 text-success"></i>{{ _('Billable Project') }}
</label>
<small class="form-text text-muted d-block">{{ _('Enable billing for this project') }}</small>
</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="hourly_rate" class="form-label fw-semibold">
<i class="fas fa-clock me-2 text-warning"></i>{{ _('Hourly Rate') }}
</label>
<input type="number" step="0.01" min="0" class="form-control" id="hourly_rate" name="hourly_rate"
value="{{ request.form.get('hourly_rate','') }}" placeholder="{{ _('e.g. 75.00') }}">
<small class="form-text text-muted">{{ _('Leave empty for non-billable projects') }}</small>
</div>
</div>
</div>
<!-- Billing Reference -->
<div class="mb-4">
<label for="billing_ref" class="form-label fw-semibold">
<i class="fas fa-receipt me-2 text-secondary"></i>{{ _('Billing Reference') }}
</label>
<input type="text" class="form-control" id="billing_ref" name="billing_ref"
value="{{ request.form.get('billing_ref','') }}" placeholder="{{ _('PO number, contract reference, etc.') }}">
<small class="form-text text-muted">{{ _('Optional: Add a reference number or identifier for billing purposes') }}</small>
</div>
<!-- Form Actions -->
<div class="d-flex gap-3 pt-3 border-top">
<button type="submit" class="btn btn-primary btn-lg">
<i class="fas fa-save me-2"></i>{{ _('Create Project') }}
</button>
<a href="{{ url_for('projects.list_projects') }}" class="btn btn-outline-secondary btn-lg">
<i class="fas fa-times me-2"></i>{{ _('Cancel') }}
</a>
</div>
</form>
</div>
</div>
</div>
<!-- Sidebar Help -->
<div class="col-lg-4">
<!-- Project Creation Tips -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-lightbulb me-2 text-warning"></i>{{ _('Project Creation Tips') }}
</h6>
</div>
<div class="card-body">
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-check text-primary fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Clear Naming') }}</small>
<small class="text-muted">{{ _("Use descriptive names that clearly indicate the project's purpose") }}</small>
</div>
</div>
</div>
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-dollar-sign text-success fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Billing Setup') }}</small>
<small class="text-muted">{{ _('Set appropriate hourly rates based on project complexity and client budget') }}</small>
</div>
</div>
</div>
<div class="tip-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-info-circle text-info fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Detailed Description') }}</small>
<small class="text-muted">{{ _('Include project objectives, deliverables, and key requirements') }}</small>
</div>
</div>
</div>
<div class="tip-item">
<div class="d-flex align-items-start">
<div class="bg-warning bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-user text-warning fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Client Selection') }}</small>
<small class="text-muted">{{ _('Choose the right client to ensure proper project organization') }}</small>
</div>
</div>
</div>
</div>
</div>
<!-- Billing Guide -->
<div class="card mobile-card mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-info-circle me-2 text-info"></i>{{ _('Billing Guide') }}
</h6>
</div>
<div class="card-body">
<div class="billing-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="billing-badge billing-billable me-2">{{ _('Billable') }}</span>
<small class="text-muted">{{ _('Track time and bill client') }}</small>
</div>
</div>
<div class="billing-guide-item mb-3">
<div class="d-flex align-items-center mb-2">
<span class="billing-badge billing-non-billable me-2">{{ _('Non-Billable') }}</span>
<small class="text-muted">{{ _('Track time without billing') }}</small>
</div>
</div>
<div class="billing-guide-item">
<div class="d-flex align-items-center mb-2">
<span class="billing-badge billing-rate me-2">{{ _('Rate Setting') }}</span>
<small class="text-muted">{{ _('Set appropriate hourly rates') }}</small>
</div>
</div>
</div>
</div>
<!-- Project Management -->
<div class="card mobile-card">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-tasks me-2 text-secondary"></i>{{ _('Project Management') }}
</h6>
</div>
<div class="card-body">
<div class="management-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-clock text-primary fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Time Tracking') }}</small>
<small class="text-muted">{{ _('Log time entries for accurate project tracking') }}</small>
</div>
</div>
</div>
<div class="management-item mb-3">
<div class="d-flex align-items-start">
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-list text-success fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Task Creation') }}</small>
<small class="text-muted">{{ _('Break down projects into manageable tasks') }}</small>
</div>
</div>
</div>
<div class="management-item">
<div class="d-flex align-items-start">
<div class="bg-info bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
<i class="fas fa-chart-bar text-info fa-xs"></i>
</div>
<div>
<small class="fw-semibold d-block">{{ _('Progress Monitoring') }}</small>
<small class="text-muted">{{ _('Track project progress and time allocation') }}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Billing Badges */
.billing-badge {
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.billing-billable {
background-color: #dcfce7;
color: #166534;
}
.billing-non-billable {
background-color: #e2e8f0;
color: #475569;
}
.billing-rate {
background-color: #fef3c7;
color: #92400e;
}
/* Tip Items */
.tip-item {
padding: 0.75rem;
border-radius: 8px;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.tip-item:hover {
background-color: #f1f5f9;
transform: translateX(4px);
}
/* Billing and Management Guide Items */
.billing-guide-item,
.management-item {
padding: 0.5rem;
border-radius: 6px;
transition: all 0.2s ease;
}
.billing-guide-item:hover,
.management-item:hover {
background-color: #f8fafc;
}
/* Form Styling */
.form-control:focus,
.form-select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
}
.form-control-lg {
min-height: 56px;
}
.form-select-lg {
min-height: 56px;
}
/* Mobile Optimizations */
@media (max-width: 768px) {
.card-header {
padding: 1rem 1rem 0.75rem 1rem;
}
.card-body {
padding: 0.75rem 1rem;
}
.tip-item:hover {
transform: none;
}
.billing-guide-item:hover,
.management-item:hover {
background-color: transparent;
}
}
/* Hover Effects */
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.tip-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
const clientSelect = document.getElementById('client_id');
const hourlyRateInput = document.getElementById('hourly_rate');
const billableCheckbox = document.getElementById('billable');
const form = document.getElementById('createProjectForm');
const nameInput = document.getElementById('name');
// Auto-fill hourly rate from client default
clientSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
const defaultRate = selectedOption.getAttribute('data-default-rate');
if (defaultRate && !hourlyRateInput.value) {
hourlyRateInput.value = defaultRate;
}
});
// Toggle hourly rate field based on billable checkbox
billableCheckbox.addEventListener('change', function() {
if (this.checked) {
hourlyRateInput.removeAttribute('disabled');
hourlyRateInput.classList.remove('text-muted');
} else {
hourlyRateInput.setAttribute('disabled', 'disabled');
hourlyRateInput.classList.add('text-muted');
hourlyRateInput.value = '';
}
});
// Form validation and enhancement
form.addEventListener('submit', function(e) {
const name = nameInput.value.trim();
const clientId = clientSelect.value;
if (!name || !clientId) {
e.preventDefault();
if (!name) {
nameInput.focus();
nameInput.classList.add('is-invalid');
}
if (!clientId) {
clientSelect.focus();
clientSelect.classList.add('is-invalid');
}
return false;
}
// Show loading state
const submitBtn = form.querySelector('button[type="submit"]');
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>Creating...';
submitBtn.disabled = true;
// Re-enable after a delay (in case of validation errors)
setTimeout(() => {
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}, 5000);
});
// Real-time validation feedback
nameInput.addEventListener('input', function() {
if (this.value.trim()) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
}
});
clientSelect.addEventListener('change', function() {
if (this.value) {
this.classList.remove('is-invalid');
this.classList.add('is-valid');
} else {
this.classList.remove('is-valid');
}
});
// Initialize form state
if (billableCheckbox.checked) {
hourlyRateInput.removeAttribute('disabled');
} else {
hourlyRateInput.setAttribute('disabled', 'disabled');
hourlyRateInput.classList.add('text-muted');
}
});
</script>
{% endblock %}