mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-25 22:19:53 -06:00
- Add per-project time entries overview page with date range filter and chronological grouping. - Add navigation links from project view/dashboard. - Group unbilled invoice time entry selection by day and sort chronologically.
467 lines
31 KiB
HTML
467 lines
31 KiB
HTML
{% extends "base.html" %}
|
|
{% from "components/ui.html" import confirm_dialog %}
|
|
|
|
{% block content %}
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold flex items-center gap-2">
|
|
<span>{{ project.name }}</span>
|
|
{% if project.code_display %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold tracking-wide uppercase bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200" title="{{ _('Project Code') }}">{{ project.code_display }}</span>
|
|
{% endif %}
|
|
</h1>
|
|
<p class="text-text-muted-light dark:text-text-muted-dark">{{ project.client }}</p>
|
|
</div>
|
|
<div class="flex gap-2 flex-wrap">
|
|
<a href="{{ url_for('projects.project_time_entries_overview', project_id=project.id) }}" class="bg-gray-200 dark:bg-gray-700 px-4 py-2 rounded-lg mt-4 md:mt-0 flex items-center gap-2 transition">
|
|
<i class="fas fa-list"></i>
|
|
{{ _('Entries Overview') }}
|
|
</a>
|
|
{% if current_user.is_admin or has_any_permission(['edit_projects', 'archive_projects']) %}
|
|
<a href="{{ url_for('projects.project_dashboard', project_id=project.id) }}" class="bg-purple-600 hover:bg-purple-700 text-white px-4 py-2 rounded-lg mt-4 md:mt-0 flex items-center gap-2 transition">
|
|
<i class="fas fa-chart-line"></i>
|
|
{{ _('Dashboard') }}
|
|
</a>
|
|
{% if project.budget_amount %}
|
|
<a href="{{ url_for('budget_alerts.project_budget_detail', project_id=project.id) }}" class="bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-4 py-2 rounded-lg mt-4 md:mt-0 flex items-center gap-2 transition shadow-lg">
|
|
<i class="fas fa-wallet"></i>
|
|
{{ _('Budget Analysis') }}
|
|
</a>
|
|
{% endif %}
|
|
{% if current_user.is_admin or has_permission('edit_projects') %}
|
|
<a href="{{ url_for('projects.edit_project', project_id=project.id) }}" class="bg-primary text-white px-4 py-2 rounded-lg mt-4 md:mt-0 transition">{{ _('Edit Project') }}</a>
|
|
{% endif %}
|
|
{% if current_user.is_admin or has_permission('edit_projects') %}
|
|
{% if project.status == 'active' %}
|
|
<form method="POST" action="{{ url_for('projects.deactivate_project', project_id=project.id) }}" onsubmit="event.preventDefault(); window.showConfirm('{{ _('Mark project as Inactive?') }}', { title: '{{ _('Change Project Status') }}', confirmText: '{{ _('Change') }}' }).then(ok=>{ if(ok) this.submit(); });" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="px-4 py-2 rounded-lg bg-amber-500 text-white mt-4 md:mt-0">{{ _('Mark Inactive') }}</button>
|
|
</form>
|
|
{% elif project.status == 'inactive' %}
|
|
<form method="POST" action="{{ url_for('projects.activate_project', project_id=project.id) }}" onsubmit="event.preventDefault(); window.showConfirm('{{ _('Activate project?') }}', { title: '{{ _('Activate Project') }}', confirmText: '{{ _('Activate') }}' }).then(ok=>{ if(ok) this.submit(); });" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="px-4 py-2 rounded-lg bg-emerald-600 text-white mt-4 md:mt-0">{{ _('Activate') }}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% if current_user.is_admin or has_permission('archive_projects') %}
|
|
{% if not project.is_archived %}
|
|
<a href="{{ url_for('projects.archive_project', project_id=project.id) }}" class="inline-block px-4 py-2 rounded-lg bg-gray-600 text-white mt-4 md:mt-0 hover:bg-gray-700 transition-colors">{{ _('Archive') }}</a>
|
|
{% else %}
|
|
<form method="POST" action="{{ url_for('projects.unarchive_project', project_id=project.id) }}" onsubmit="event.preventDefault(); window.showConfirm('{{ _('Unarchive project?') }}', { title: '{{ _('Unarchive Project') }}', confirmText: '{{ _('Unarchive') }}' }).then(ok=>{ if(ok) this.submit(); });" class="inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="px-4 py-2 rounded-lg bg-sky-600 text-white mt-4 md:mt-0">{{ _('Unarchive') }}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endif %}
|
|
<button type="button" class="bg-red-600 text-white px-4 py-2 rounded-lg mt-4 md:mt-0"
|
|
onclick="document.getElementById('confirmDeleteProject-{{ project.id }}').classList.remove('hidden')">
|
|
{{ _('Delete Project') }}
|
|
</button>
|
|
<form id="confirmDeleteProject-{{ project.id }}-form" method="POST" action="{{ url_for('projects.delete_project', project_id=project.id) }}" class="hidden">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Left Column: Project Details -->
|
|
<div class="lg:col-span-1 space-y-6">
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
|
<h2 class="text-lg font-semibold mb-4">Details</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">{{ _('Project Code') }}</h3>
|
|
<p>
|
|
{% if project.code_display %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[10px] font-semibold tracking-wide uppercase bg-gray-200 text-gray-800 dark:bg-gray-700 dark:text-gray-200">{{ project.code_display }}</span>
|
|
{% else %}
|
|
—
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Description</h3>
|
|
<div class="prose prose-sm dark:prose-invert max-w-none">{{ (project.description or 'No description provided.') | markdown | safe }}</div>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Status</h3>
|
|
{% set status_map = {
|
|
'active': {'cls': 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300', 'label': _('Active')},
|
|
'inactive': {'cls': 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300', 'label': _('Inactive')},
|
|
'archived': {'cls': 'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200', 'label': _('Archived')},
|
|
} %}
|
|
{% set st = status_map.get(project.status, status_map['inactive']) %}
|
|
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap {{ st.cls }}">{{ st.label }}</span>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">Billing</h3>
|
|
<p>{{ 'Billable' if project.billable else 'Not Billable' }} {% if project.hourly_rate %}({{ "%.2f"|format(project.hourly_rate) }}/hr){% endif %}</p>
|
|
</div>
|
|
{% if project.is_archived and project.archived_at %}
|
|
<div class="pt-4 border-t border-border-light dark:border-border-dark">
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark mb-2">{{ _('Archive Information') }}</h3>
|
|
<div class="space-y-2 text-sm">
|
|
<div>
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Archived on:') }}</span>
|
|
<span class="font-medium">{{ project.archived_at|user_datetime('%Y-%m-%d %H:%M') }}</span>
|
|
</div>
|
|
{% if project.archived_by_user %}
|
|
<div>
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Archived by:') }}</span>
|
|
<span class="font-medium">{{ project.archived_by_user.full_name or project.archived_by_user.username }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if project.archived_reason %}
|
|
<div>
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Reason:') }}</span>
|
|
<p class="mt-1 p-2 bg-gray-50 dark:bg-gray-800 rounded text-gray-700 dark:text-gray-300">{{ project.archived_reason }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Custom Fields Section -->
|
|
{% if project.custom_fields %}
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
|
<h2 class="text-lg font-semibold mb-4">{{ _('Custom Fields') }}</h2>
|
|
<div class="space-y-3">
|
|
{% for key, value in project.custom_fields.items() %}
|
|
<div>
|
|
{% set field_defs = custom_field_definitions_by_key|default({}) %}
|
|
{% set field_definition = field_defs[key] if key in field_defs else None %}
|
|
<h3 class="text-sm font-medium text-text-muted-light dark:text-text-muted-dark">
|
|
{% if field_definition %}{{ field_definition.label }}{% else %}{{ key }}{% endif %}
|
|
</h3>
|
|
<p class="text-text-light dark:text-text-dark">
|
|
{% set link_template = link_templates_by_field.get(key) if link_templates_by_field else None %}
|
|
{% if link_template and value %}
|
|
{# Render as clickable link using link template #}
|
|
{% set rendered_url = link_template.render_url(value) %}
|
|
{% if rendered_url %}
|
|
<a href="{{ rendered_url }}" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline break-all">
|
|
{{ value }}
|
|
<i class="fas fa-external-link-alt ml-1 text-xs"></i>
|
|
</a>
|
|
{% else %}
|
|
{{ value }}
|
|
{% endif %}
|
|
{% elif value is string and (value.startswith('http://') or value.startswith('https://')) %}
|
|
{# Fallback: If the value looks like a URL, render it as a clickable link #}
|
|
<a href="{{ value }}" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline break-all">
|
|
{{ value }}
|
|
<i class="fas fa-external-link-alt ml-1 text-xs"></i>
|
|
</a>
|
|
{% elif value is string and value.startswith('www.') %}
|
|
{# Handle www. URLs #}
|
|
<a href="https://{{ value }}" target="_blank" rel="noopener noreferrer" class="text-primary hover:underline break-all">
|
|
{{ value }}
|
|
<i class="fas fa-external-link-alt ml-1 text-xs"></i>
|
|
</a>
|
|
{% else %}
|
|
{{ value }}
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Rendered Links from Link Templates -->
|
|
{% set rendered_links = project.get_rendered_links() %}
|
|
{% if rendered_links %}
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
|
<h2 class="text-lg font-semibold mb-4">{{ _('Quick Links') }}</h2>
|
|
<div class="space-y-2">
|
|
{% for link in rendered_links %}
|
|
<a href="{{ link.url }}" target="_blank" rel="noopener noreferrer" class="flex items-center gap-2 text-primary hover:text-primary-dark hover:underline">
|
|
{% if link.icon %}
|
|
<i class="{{ link.icon }}"></i>
|
|
{% else %}
|
|
<i class="fas fa-external-link-alt"></i>
|
|
{% endif %}
|
|
<span>{{ link.name }}</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Budget Overview Card -->
|
|
{% if project.budget_amount %}
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow animated-card">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<h2 class="text-lg font-semibold flex items-center">
|
|
<i class="fas fa-wallet mr-2 text-green-500"></i>
|
|
{{ _('Budget Overview') }}
|
|
</h2>
|
|
<a href="{{ url_for('budget_alerts.project_budget_detail', project_id=project.id) }}"
|
|
class="text-primary hover:text-primary-dark text-sm flex items-center gap-1 transition">
|
|
{{ _('Details') }} <i class="fas fa-arrow-right text-xs"></i>
|
|
</a>
|
|
</div>
|
|
|
|
{% set consumed_amount = project.budget_consumed_amount|float if project.budget_consumed_amount else 0.0 %}
|
|
{% set budget_amt = project.budget_amount|float %}
|
|
{% set remaining = budget_amt - consumed_amount %}
|
|
{% set percentage = (consumed_amount / budget_amt * 100) if budget_amt > 0 else 0 %}
|
|
{% set threshold = project.budget_threshold_percent or 80 %}
|
|
|
|
<!-- Budget Progress Bar -->
|
|
<div class="mb-4">
|
|
<div class="flex justify-between text-sm mb-2">
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Budget Used') }}</span>
|
|
<span class="font-semibold">{{ "%.1f"|format(percentage) }}%</span>
|
|
</div>
|
|
<div class="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
|
<div class="h-full {% if percentage >= 100 %}bg-gradient-to-r from-red-500 to-pink-600{% elif percentage >= threshold %}bg-gradient-to-r from-yellow-400 to-orange-500{% else %}bg-gradient-to-r from-green-500 to-emerald-600{% endif %} transition-all duration-300 rounded-full"
|
|
style="width: {{ [percentage, 100]|min }}%">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Budget Stats Grid -->
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div class="bg-blue-50 dark:bg-blue-900/20 p-3 rounded-lg">
|
|
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mb-1">{{ _('Total Budget') }}</div>
|
|
<div class="text-lg font-bold text-blue-600 dark:text-blue-400">${{ "%.2f"|format(budget_amt) }}</div>
|
|
</div>
|
|
<div class="bg-amber-50 dark:bg-amber-900/20 p-3 rounded-lg">
|
|
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mb-1">{{ _('Consumed') }}</div>
|
|
<div class="text-lg font-bold text-amber-600 dark:text-amber-400">${{ "%.2f"|format(consumed_amount) }}</div>
|
|
</div>
|
|
<div class="{% if remaining >= 0 %}bg-green-50 dark:bg-green-900/20{% else %}bg-red-50 dark:bg-red-900/20{% endif %} p-3 rounded-lg">
|
|
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mb-1">{{ _('Remaining') }}</div>
|
|
<div class="text-lg font-bold {% if remaining >= 0 %}text-green-600 dark:text-green-400{% else %}text-red-600 dark:text-red-400{% endif %}">
|
|
${{ "%.2f"|format(remaining|abs) }}
|
|
{% if remaining < 0 %}<span class="text-xs">{{ _('over') }}</span>{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="bg-purple-50 dark:bg-purple-900/20 p-3 rounded-lg">
|
|
<div class="text-xs text-text-muted-light dark:text-text-muted-dark mb-1">{{ _('Status') }}</div>
|
|
<div class="flex items-center gap-1">
|
|
{% if percentage >= 100 %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300">
|
|
<i class="fas fa-exclamation-circle mr-1"></i>{{ _('Over') }}
|
|
</span>
|
|
{% elif percentage >= threshold %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300">
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>{{ _('Critical') }}
|
|
</span>
|
|
{% elif percentage >= (threshold * 0.8) %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/50 dark:text-amber-300">
|
|
<i class="fas fa-info-circle mr-1"></i>{{ _('Warning') }}
|
|
</span>
|
|
{% else %}
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300">
|
|
<i class="fas fa-check-circle mr-1"></i>{{ _('Healthy') }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Action Button -->
|
|
<div class="mt-4 pt-4 border-t border-border-light dark:border-border-dark">
|
|
<a href="{{ url_for('budget_alerts.project_budget_detail', project_id=project.id) }}"
|
|
class="block w-full text-center bg-gradient-to-r from-green-600 to-emerald-600 hover:from-green-700 hover:to-emerald-700 text-white px-4 py-2 rounded-lg transition shadow-md hover:shadow-lg">
|
|
<i class="fas fa-chart-line mr-2"></i>{{ _('View Full Budget Analysis') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
|
<h2 class="text-lg font-semibold mb-4">User Contributions</h2>
|
|
<ul>
|
|
{% for user_total in user_totals %}
|
|
<li class="flex justify-between py-2 border-b border-border-light dark:border-border-dark">
|
|
<span>{{ user_total.username }}</span>
|
|
<span class="font-semibold">{{ "%.2f"|format(user_total.total_hours) }} hrs</span>
|
|
</li>
|
|
{% else %}
|
|
<li>No hours logged yet.</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Attachments Section -->
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h2 class="text-lg font-semibold">
|
|
<i class="fas fa-paperclip mr-2"></i>{{ _('Attachments') }}
|
|
{% if attachments %}
|
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">({{ attachments|length }})</span>
|
|
{% endif %}
|
|
</h2>
|
|
{% if current_user.is_admin or has_permission('edit_projects') %}
|
|
<button type="button" onclick="document.getElementById('upload-attachment-form').classList.toggle('hidden')" class="bg-primary text-white px-3 py-1.5 rounded-lg hover:bg-primary-dark text-sm">
|
|
<i class="fas fa-plus mr-1"></i>{{ _('Upload') }}
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Upload Form -->
|
|
{% if current_user.is_admin or has_permission('edit_projects') %}
|
|
<div id="upload-attachment-form" class="hidden mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<form method="POST" action="{{ url_for('projects.upload_project_attachment', project_id=project.id) }}" enctype="multipart/form-data" class="space-y-3">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div>
|
|
<label for="attachment-file" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('File') }}</label>
|
|
<input type="file" name="file" id="attachment-file" class="w-full form-input" required>
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">{{ _('Max size: 10 MB. Allowed: images, PDFs, documents, spreadsheets, archives') }}</p>
|
|
</div>
|
|
<div>
|
|
<label for="attachment-description" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{{ _('Description (optional)') }}</label>
|
|
<input type="text" name="description" id="attachment-description" class="w-full form-input" placeholder="{{ _('e.g., Contract, Specification, etc.') }}">
|
|
</div>
|
|
<div class="flex items-center">
|
|
<label class="flex items-center">
|
|
<input type="checkbox" name="is_visible_to_client" value="true" class="mr-2">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">{{ _('Visible to client in portal') }}</span>
|
|
</label>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button type="submit" class="bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary-dark text-sm">
|
|
<i class="fas fa-upload mr-1"></i>{{ _('Upload') }}
|
|
</button>
|
|
<button type="button" onclick="document.getElementById('upload-attachment-form').classList.add('hidden')" class="bg-gray-500 text-white px-4 py-2 rounded-lg hover:bg-gray-600 text-sm">
|
|
{{ _('Cancel') }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Attachments List -->
|
|
<div class="space-y-2">
|
|
{% if attachments %}
|
|
{% for attachment in attachments %}
|
|
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
<div class="flex-shrink-0">
|
|
{% if attachment.is_pdf %}
|
|
<i class="fas fa-file-pdf text-red-500 text-xl"></i>
|
|
{% elif attachment.is_image %}
|
|
<i class="fas fa-file-image text-blue-500 text-xl"></i>
|
|
{% elif attachment.is_document %}
|
|
<i class="fas fa-file-word text-blue-600 text-xl"></i>
|
|
{% else %}
|
|
<i class="fas fa-file text-gray-500 text-xl"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<a href="{{ url_for('projects.download_project_attachment', attachment_id=attachment.id) }}" class="text-primary hover:underline font-medium block truncate" title="{{ attachment.original_filename }}">
|
|
{{ attachment.original_filename }}
|
|
</a>
|
|
{% if attachment.description %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">{{ attachment.description }}</p>
|
|
{% endif %}
|
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
|
{{ attachment.file_size_display }} • {{ attachment.uploaded_at|user_datetime('%Y-%m-%d %H:%M') if attachment.uploaded_at else '' }}
|
|
{% if attachment.is_visible_to_client %}
|
|
<span class="ml-2 px-1.5 py-0.5 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs">{{ _('Client Visible') }}</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{% if current_user.is_admin or has_permission('edit_projects') %}
|
|
<form method="POST" action="{{ url_for('projects.delete_project_attachment', attachment_id=attachment.id) }}" class="ml-2" onsubmit="event.preventDefault(); window.showConfirm('{{ _('Are you sure you want to delete this attachment?') }}', { title: '{{ _('Delete Attachment') }}', confirmText: '{{ _('Delete') }}', variant: 'danger' }).then(ok=>{ if(ok) this.submit(); });">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="text-red-500 hover:text-red-700 p-1" title="{{ _('Delete') }}">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 text-center py-4">{{ _('No attachments yet') }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Tabs for Tasks, Entries, etc. -->
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow p-6 overflow-x-auto">
|
|
<h3 class="text-lg font-semibold mb-4">{{ _('Tasks for this project') }}</h3>
|
|
<table class="w-full text-left">
|
|
<thead class="border-b border-border-light dark:border-border-dark">
|
|
<tr>
|
|
<th class="p-3">{{ _('Name') }}</th>
|
|
<th class="p-3">{{ _('Priority') }}</th>
|
|
<th class="p-3">{{ _('Status') }}</th>
|
|
<th class="p-3">{{ _('Due') }}</th>
|
|
<th class="p-3">{{ _('Progress') }}</th>
|
|
<th class="p-3">{{ _('Actions') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for task in tasks %}
|
|
<tr class="border-b border-border-light dark:border-border-dark">
|
|
<td class="p-3">{{ task.name }}</td>
|
|
<td class="p-3">
|
|
{% set p = task.priority %}
|
|
{% set pcls = {'low':'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
|
|
'medium':'bg-sky-100 text-sky-700 dark:bg-sky-900/30 dark:text-sky-300',
|
|
'high':'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
|
'urgent':'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300'}[p] if p in ['low','medium','high','urgent'] else 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300' %}
|
|
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap {{ pcls }}">{{ task.priority_display }}</span>
|
|
</td>
|
|
<td class="p-3">
|
|
{% set s = task.status %}
|
|
{% set scls = {'todo':'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300',
|
|
'in_progress':'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
|
'review':'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300',
|
|
'done':'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300',
|
|
'cancelled':'bg-gray-100 text-gray-700 dark:bg-gray-700 dark:text-gray-200'}[s] if s in ['todo','in_progress','review','done','cancelled'] else 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300' %}
|
|
<span class="px-2 py-1 rounded-full text-xs font-medium whitespace-nowrap {{ scls }}">{{ task.status_display }}</span>
|
|
</td>
|
|
<td class="p-3">
|
|
{% if task.due_date %}
|
|
{% set overdue = task.is_overdue %}
|
|
<span class="px-2 py-1 rounded-md text-xs font-medium whitespace-nowrap {{ 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300' if overdue else 'bg-primary/10 text-primary' }}">{{ task.due_date.strftime('%Y-%m-%d') }}</span>
|
|
{% else %}
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">—</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="p-3">
|
|
{% set pct = task.progress_percentage or 0 %}
|
|
<div class="w-28 h-2 bg-gray-200 dark:bg-gray-700 rounded">
|
|
<div class="h-2 rounded {{ 'bg-emerald-500' if pct>=100 else 'bg-primary' }}" style="width: {{ [pct,100]|min }}%"></div>
|
|
</div>
|
|
</td>
|
|
<td class="p-3">
|
|
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="text-primary hover:underline">{{ _('View') }}</a>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="p-3 text-center text-text-muted-light dark:text-text-muted-dark">{{ _('No tasks for this project.') }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% if current_user.is_admin or has_permission('delete_projects') %}
|
|
{{ confirm_dialog(
|
|
'confirmDeleteProject-' ~ project.id,
|
|
'Delete Project',
|
|
'Are you sure you want to delete this project? This action cannot be undone.',
|
|
'Delete',
|
|
'Cancel',
|
|
'danger'
|
|
) }}
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block scripts_extra %}{% endblock %}
|