Prepare ERP source for Gitea deployment
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Document Download Requests</h1>
|
||||
<p class="text-sm text-slate-500">Secure local-to-cloud retrieval queue for files stored on branch storage nodes.</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Jobs</a>
|
||||
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-900 text-white text-sm font-semibold">Documents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="rounded-2xl border bg-white p-4 flex gap-3 items-end">
|
||||
<div><label class="text-sm font-medium">Status</label><input name="status" value="{{ status }}" class="mt-1 rounded-xl border p-2" placeholder="pending / ready / failed"></div>
|
||||
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 font-semibold">Filter</button>
|
||||
<a href="/documents/download-requests" class="rounded-xl bg-slate-100 text-slate-700 px-4 py-2 font-semibold">Clear</a>
|
||||
</form>
|
||||
|
||||
<div class="rounded-2xl border bg-white overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Request</th><th class="p-3 text-left">Document</th><th class="p-3 text-left">Version</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Local Path</th><th class="p-3 text-left">Created</th><th class="p-3 text-right">Action</th></tr></thead>
|
||||
<tbody>
|
||||
{% for item in requests %}
|
||||
<tr class="border-t">
|
||||
<td class="p-3 font-semibold">#{{ item.id }}</td>
|
||||
<td class="p-3">{{ item.document.title if item.document else item.document_id }}</td>
|
||||
<td class="p-3">{{ item.version.version_no if item.version else item.version_id }}</td>
|
||||
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs bg-slate-100 text-slate-700">{{ item.request_status.replace('_',' ').title() }}</span>{% if item.last_error %}<div class="mt-1 text-xs text-red-600">{{ item.last_error }}</div>{% endif %}</td>
|
||||
<td class="p-3 font-mono text-xs break-all">{{ item.local_relative_path or '-' }}</td>
|
||||
<td class="p-3">{{ item.created_at_utc }}</td>
|
||||
<td class="p-3 text-right">{% if item.request_status == 'ready' %}<a href="/documents/download-requests/{{ item.id }}/download" class="font-semibold text-brand-700 hover:underline">Download</a>{% else %}<span class="text-xs text-slate-400">Not ready</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="7" class="p-6 text-center text-slate-500">No download requests found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,137 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Engagement Documents</h2>
|
||||
<p class="text-sm text-slate-500">{{ engagement.client.client_name if engagement.client else '-' }} · {{ engagement.catalogue.service_name if engagement.catalogue else '-' }} · FY {{ engagement.financial_year }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/services/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement</a>
|
||||
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Document uploaded successfully.</div>{% endif %}
|
||||
{% if request.query_params.get('deleted') %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Document archived successfully.</div>{% endif %}
|
||||
{% if request.query_params.get('download_queued') %}<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">The file is stored in branch local storage. A secure retrieval request #{{ request.query_params.get('download_queued') }} has been queued. Please refresh after the local storage app fulfils it.</div>{% endif %}
|
||||
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check file size, permission and storage path.</div>{% endif %}
|
||||
|
||||
{% if can_upload %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Upload Document / New Revision</h3>
|
||||
<form method="post" action="/documents/engagements/{{ engagement.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
|
||||
<input type="text" name="title" placeholder="Document title" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Document Type</label>
|
||||
<select name="document_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for item in document_types %}<option value="{{ item }}">{{ item.replace('_',' ').title() }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
|
||||
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Create new document</option>
|
||||
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
|
||||
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
|
||||
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
|
||||
<textarea name="remarks" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Document</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Type</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Version</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Storage</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Revision History</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for doc in documents %}
|
||||
{% set latest = doc.versions[0] if doc.versions else None %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_code }}{% if doc.description %} · {{ doc.description }}{% endif %}</div></td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ doc.document_type.replace('_',' ').title() }}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div>v{{ latest.version_no }} · {{ latest.original_filename }}</div><div>{{ (latest.file_size_bytes / 1024)|round(1) }} KB · {{ latest.uploaded_at_utc }}</div>{% else %}-{% endif %}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
{% if latest %}
|
||||
<div><span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ latest.storage_status.replace('_',' ').title() }}</span></div>
|
||||
<div class="mt-1 font-mono text-[11px] break-all">{{ latest.local_relative_path }}</div>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">
|
||||
{% for v in doc.versions %}
|
||||
<div><a href="/documents/versions/{{ v.id }}/download" class="text-brand-700 hover:underline">v{{ v.version_no }}</a> · {{ v.original_filename }}{% if v.remarks %} · {{ v.remarks }}{% endif %}</div>
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right text-sm">
|
||||
{% if latest %}<a href="/documents/{{ doc.id }}/download" class="font-medium text-brand-700 hover:underline">Download Latest</a>{% endif %}
|
||||
{% if can_upload %}
|
||||
<form method="post" action="/documents/{{ doc.id }}/delete" class="ml-3 inline" onsubmit="return confirm('Archive this document?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button class="font-medium text-red-600 hover:underline">Archive</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No documents uploaded for this engagement.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if download_requests %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold text-slate-900">Recent Local Storage Retrieval Requests</h3>
|
||||
<p class="text-xs text-slate-500">Used when the ERP staged file is unavailable and the file must be pulled back from branch local storage.</p>
|
||||
</div>
|
||||
<a href="/documents/download-requests" class="text-sm font-medium text-brand-700 hover:underline">View All</a>
|
||||
</div>
|
||||
<div class="mt-4 overflow-hidden rounded-xl border border-slate-200">
|
||||
<table class="min-w-full divide-y divide-slate-200 text-sm">
|
||||
<thead class="bg-slate-50 text-xs uppercase tracking-wide text-slate-500"><tr><th class="px-3 py-2 text-left">Request</th><th class="px-3 py-2 text-left">Version</th><th class="px-3 py-2 text-left">Status</th><th class="px-3 py-2 text-left">Created</th><th class="px-3 py-2 text-right">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for req in download_requests %}
|
||||
<tr>
|
||||
<td class="px-3 py-2 font-medium">#{{ req.id }}</td>
|
||||
<td class="px-3 py-2">v{{ req.version.version_no if req.version else req.version_id }}</td>
|
||||
<td class="px-3 py-2"><span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-700">{{ req.request_status.replace('_',' ').title() }}</span></td>
|
||||
<td class="px-3 py-2 text-xs text-slate-500">{{ req.created_at_utc }}</td>
|
||||
<td class="px-3 py-2 text-right">{% if req.request_status == 'ready' %}<a href="/documents/download-requests/{{ req.id }}/download" class="font-medium text-brand-700 hover:underline">Download</a>{% else %}<span class="text-xs text-slate-400">Waiting for local app</span>{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Engagement Documents</h2>
|
||||
<p class="text-sm text-slate-500">Upload, stream, download and version documents engagement-wise with year-wise storage.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Financial Year</label>
|
||||
<input type="text" name="financial_year" value="{{ financial_year or '' }}" placeholder="2025-26" class="w-36 rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search</label>
|
||||
<input type="text" name="q" value="{{ q or '' }}" placeholder="Client, service or code" class="w-80 rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Filter</button>
|
||||
</form>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<a href="/documents/permanent" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Permanent Client Documents</a>
|
||||
<a href="/documents/storage-nodes" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Branch Storage Nodes</a>
|
||||
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 hover:bg-slate-200 text-sm font-semibold">Storage Jobs</a>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Engagement</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">FY / AY</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Assigned</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for row in engagements %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ row.client.client_name if row.client else '-' }}</div><div class="text-xs text-slate-500">{{ row.client.client_code if row.client else '' }}</div></td>
|
||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ row.catalogue.service_name if row.catalogue else '-' }}</div><div class="text-xs text-slate-500">{{ row.catalogue.service_code if row.catalogue else '' }}</div></td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600"><div>FY: {{ row.financial_year or '-' }}</div><div>AY: {{ row.assessment_year or '-' }}</div></td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600"><div>Partner: {{ row.assigned_partner.full_name if row.assigned_partner and row.assigned_partner.full_name else (row.assigned_partner.email if row.assigned_partner else '-') }}</div><div>Staff: {{ row.assigned_staff.full_name if row.assigned_staff and row.assigned_staff.full_name else (row.assigned_staff.email if row.assigned_staff else '-') }}</div></td>
|
||||
<td class="px-4 py-3 text-right text-sm"><a href="/documents/engagements/{{ row.id }}" class="font-medium text-brand-700 hover:underline">Open Documents</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="px-4 py-8 text-center text-sm text-slate-500">No engagements available for document access.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,97 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Permanent Client Documents</h2>
|
||||
<p class="text-sm text-slate-500">{{ client.client_name }} · {{ client.client_code or '-' }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/documents/permanent" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Back</a>
|
||||
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Permanent document uploaded successfully.</div>{% endif %}
|
||||
{% if request.query_params.get('deleted') %}<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">Permanent document archived successfully.</div>{% endif %}
|
||||
{% if request.query_params.get('download_queued') %}<div class="rounded-2xl border border-sky-200 bg-sky-50 p-4 text-sm text-sky-800">The file is stored in branch local storage. Retrieval request #{{ request.query_params.get('download_queued') }} has been queued.</div>{% endif %}
|
||||
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Action failed. Please check file size, permission and storage path.</div>{% endif %}
|
||||
|
||||
{% if can_upload %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Upload Permanent Document / New Revision</h3>
|
||||
<form method="post" action="/documents/permanent/clients/{{ client.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
|
||||
<input type="text" name="title" placeholder="Document title" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Category</label>
|
||||
<select name="category" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for item in permanent_document_categories %}<option value="{{ item }}">{{ item }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
|
||||
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Create new document</option>
|
||||
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
|
||||
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
|
||||
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
|
||||
<textarea name="remarks" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button></div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Document</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Category</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Latest Version</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Storage Path</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Versions</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for doc in documents %}
|
||||
{% set latest = doc.versions[0] if doc.versions else None %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_code }}{% if doc.description %} · {{ doc.description }}{% endif %}</div></td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ doc.category }}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div>v{{ latest.version_no }} · {{ latest.original_filename }}</div><div>{{ (latest.file_size_bytes / 1024)|round(1) }} KB · {{ latest.uploaded_at_utc }}</div>{% else %}-{% endif %}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% if latest %}<div><span class="rounded-full bg-slate-100 px-2 py-1 text-slate-700">{{ latest.storage_status.replace('_',' ').title() }}</span></div><div class="mt-1 font-mono text-[11px] break-all">{{ latest.local_relative_path }}</div>{% else %}-{% endif %}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% for v in doc.versions %}<div><a href="/documents/permanent/versions/{{ v.id }}/download" class="text-brand-700 hover:underline">v{{ v.version_no }}</a> · {{ v.original_filename }}{% if v.remarks %} · {{ v.remarks }}{% endif %}</div>{% endfor %}</td>
|
||||
<td class="px-4 py-3 text-right text-sm">
|
||||
{% if latest %}<a href="/documents/permanent/{{ doc.id }}/download" class="font-medium text-brand-700 hover:underline">Download Latest</a>{% endif %}
|
||||
{% if can_upload %}
|
||||
<form method="post" action="/documents/permanent/{{ doc.id }}/delete" class="ml-3 inline" onsubmit="return confirm('Archive this permanent document?');">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button class="font-medium text-red-600 hover:underline">Archive</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="px-4 py-8 text-center text-sm text-slate-500">No permanent documents uploaded for this client.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Permanent Client Documents</h2>
|
||||
<p class="text-sm text-slate-500">Store standing records like registration documents, GST, PAN/TAN, KYC, bank and agreements outside financial-year folders.</p>
|
||||
</div>
|
||||
<a href="/documents" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
|
||||
</div>
|
||||
|
||||
<form method="get" class="flex flex-wrap items-end gap-3 rounded-2xl bg-white p-4 shadow-soft">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Search Client</label>
|
||||
<input type="text" name="q" value="{{ q or '' }}" placeholder="Client name or code" class="w-80 rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<button class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Filter</button>
|
||||
</form>
|
||||
|
||||
<div class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Client</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Code</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-wide text-slate-500">Branch</th>
|
||||
<th class="px-4 py-3 text-right text-xs font-semibold uppercase tracking-wide text-slate-500">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for client in clients %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm font-medium text-slate-900">{{ client.client_name }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">{{ client.client_code or '-' }}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-600">{{ client.branch_id or '-' }}</td>
|
||||
<td class="px-4 py-3 text-right text-sm"><a href="/documents/permanent/clients/{{ client.id }}" class="font-medium text-brand-700 hover:underline">Open Permanent Vault</a></td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No clients available for permanent document access.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Document Storage Jobs</h1>
|
||||
<p class="text-sm text-slate-500">Cloud-to-local streaming queue for branch storage nodes.</p>
|
||||
</div>
|
||||
<div class="flex gap-2"><a href="/documents/download-requests" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Download Requests</a><a href="/documents/storage-nodes" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Nodes</a></div>
|
||||
</div>
|
||||
<form method="get" class="rounded-2xl border bg-white p-4 flex gap-3 items-end">
|
||||
<div><label class="text-sm font-medium">Status</label><input name="status" value="{{ status }}" class="mt-1 rounded-xl border p-2" placeholder="pending / completed / failed"></div>
|
||||
<button class="rounded-xl bg-slate-900 text-white px-4 py-2 font-semibold">Filter</button>
|
||||
<a href="/documents/storage-jobs" class="rounded-xl bg-slate-100 text-slate-700 px-4 py-2 font-semibold">Clear</a>
|
||||
</form>
|
||||
<div class="rounded-2xl border bg-white overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Job</th><th class="p-3 text-left">Version</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Target Path</th><th class="p-3 text-left">Created</th></tr></thead>
|
||||
<tbody>
|
||||
{% for job in jobs %}
|
||||
<tr class="border-t">
|
||||
<td class="p-3 font-semibold">#{{ job.id }}</td>
|
||||
<td class="p-3">{{ job.version_id }}</td>
|
||||
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs bg-slate-100 text-slate-700">{{ job.status }}</span></td>
|
||||
<td class="p-3 font-mono text-xs break-all">{{ job.target_relative_path }}</td>
|
||||
<td class="p-3">{{ job.created_at_utc }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="5" class="p-6 text-center text-slate-500">No storage jobs found.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,199 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-slate-900">Branch Local Storage</h1>
|
||||
<p class="text-sm text-slate-500">Partner/branch controlled storage setup for engagement document streaming.</p>
|
||||
<p class="text-xs text-slate-400 mt-1">Scope: {{ storage_scope_title or '-' }}</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/documents" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Back to Documents</a>
|
||||
<a href="/documents/storage-jobs" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Storage Jobs</a>
|
||||
<a href="/documents/download-requests" class="px-3 py-2 rounded-xl bg-slate-100 text-slate-700 text-sm font-semibold">Download Requests</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('error') == 'branch_not_linked' %}
|
||||
<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">
|
||||
Your user is not linked to a branch. Please update the Partner/Branch Manager user profile with branch_id before enabling local storage.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-xs text-slate-500">Storage Nodes</div>
|
||||
<div class="mt-1 text-2xl font-bold text-slate-900">{{ nodes|length }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-xs text-slate-500">Online / Active</div>
|
||||
<div class="mt-1 text-2xl font-bold text-emerald-700">{{ nodes|selectattr('is_active')|list|length }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-xs text-slate-500">Recent Storage Jobs</div>
|
||||
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_jobs|length if recent_jobs is defined else 0 }}</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border bg-white p-4 shadow-sm">
|
||||
<div class="text-xs text-slate-500">Recent Download Requests</div>
|
||||
<div class="mt-1 text-2xl font-bold text-slate-900">{{ recent_download_requests|length if recent_download_requests is defined else 0 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('created') == '1' %}
|
||||
<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">
|
||||
Storage node created. Use the <strong>Download Agent Package</strong> button in the node row to generate the branch package. Secret and standalone .env are not displayed.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if request.query_params.get('error') == 'env_download_disabled' %}
|
||||
<div class="rounded-2xl border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
Standalone .env download is disabled for security. Download the pre-configured agent ZIP instead.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||
<div class="space-y-6">
|
||||
{% if can_manage_branch_storage %}
|
||||
<form method="post" action="/documents/storage-nodes/auto-setup" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<h2 class="font-bold text-slate-900">One-click branch setup</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">Recommended. ERP will create or reuse the branch node and immediately download the pre-configured LSA2 Windows Service ZIP. Secret is not shown on screen. If a branch already has a storage root, the existing root will be reused and will not be silently changed.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-sm font-medium">Branch</label>
|
||||
{% if forced_branch_id %}
|
||||
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
|
||||
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
|
||||
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
|
||||
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
|
||||
<option value="">Firm-level / Head office storage</option>
|
||||
{% for branch in branches or [] %}
|
||||
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" value="D:\AuditFirmStorage"><p class="text-xs text-amber-700 mt-1">For a new branch setup only. Existing branch nodes keep their already configured root.</p></div>
|
||||
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
|
||||
<button class="w-full rounded-xl bg-emerald-700 text-white py-2 font-semibold">Download / Regenerate Branch Agent Package</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/documents/storage-nodes" class="rounded-2xl border bg-white p-5 space-y-4 shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<h2 class="font-bold text-slate-900">Manual node creation</h2>
|
||||
<p class="text-xs text-slate-500 mt-1">Use only when you want a custom node code/name.</p>
|
||||
</div>
|
||||
<div><label class="text-sm font-medium">Node Code</label><input name="node_code" required class="mt-1 w-full rounded-xl border p-2" placeholder="CHN-STORAGE-01"></div>
|
||||
<div><label class="text-sm font-medium">Node Name</label><input name="node_name" required class="mt-1 w-full rounded-xl border p-2" placeholder="Chennai Branch Storage"></div>
|
||||
<div>
|
||||
<label class="text-sm font-medium">Branch</label>
|
||||
{% if forced_branch_id %}
|
||||
<input type="hidden" name="branch_id" value="{{ forced_branch_id }}">
|
||||
<div class="mt-1 rounded-xl border bg-slate-50 p-2 text-sm text-slate-700">
|
||||
{% set fb = branch_map.get(forced_branch_id) if branch_map else None %}
|
||||
{{ fb.name if fb else ('Branch ID ' ~ forced_branch_id) }}{% if fb and fb.code %} ({{ fb.code }}){% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<select name="branch_id" class="mt-1 w-full rounded-xl border p-2">
|
||||
<option value="">Firm-level / Head office storage</option>
|
||||
{% for branch in branches or [] %}
|
||||
<option value="{{ branch.id }}">{{ branch.name }}{% if branch.code %} ({{ branch.code }}){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div><label class="text-sm font-medium">Connector URL</label><input name="connector_url" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional zero-trust tunnel URL"></div>
|
||||
<div><label class="text-sm font-medium">Local Storage Root</label><input name="storage_root_path" class="mt-1 w-full rounded-xl border p-2" placeholder="D:\AuditFirmStorage"></div>
|
||||
<div><label class="text-sm font-medium">Quota GB</label><input name="quota_limit_gb" class="mt-1 w-full rounded-xl border p-2" placeholder="Optional"></div>
|
||||
<button class="w-full rounded-xl bg-slate-900 text-white py-2 font-semibold">Create Node</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="rounded-2xl border bg-slate-50 p-5 text-sm text-slate-600">
|
||||
Storage setup package generation is available to Firm Admin, Partner and Branch Manager. This screen is monitoring-only for your current role.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="xl:col-span-2 space-y-6">
|
||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="font-bold text-slate-900">Configured storage nodes</h2>
|
||||
<p class="text-xs text-slate-500">Partners/Branch Managers see only their managed branch. One branch should have one active node and one fixed storage root. Existing node secrets are not shown again.</p>
|
||||
</div>
|
||||
</div>
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-3 text-left">Node</th><th class="p-3 text-left">Branch</th><th class="p-3 text-left">Status</th><th class="p-3 text-left">Last Seen</th><th class="p-3 text-left">Root</th><th class="p-3 text-right">Action</th></tr></thead>
|
||||
<tbody>
|
||||
{% for node in nodes %}
|
||||
{% set b = branch_map.get(node.branch_id) if branch_map and node.branch_id else None %}
|
||||
<tr class="border-t align-top">
|
||||
<td class="p-3"><div class="font-semibold">{{ node.node_name }}</div><div class="text-xs text-slate-500">{{ node.node_code }}</div></td>
|
||||
<td class="p-3">{{ b.name if b else (node.branch_id or 'Firm-level') }}</td>
|
||||
<td class="p-3"><span class="px-2 py-1 rounded-full text-xs {{ 'bg-emerald-100 text-emerald-700' if node.is_active else 'bg-slate-100 text-slate-600' }}">{{ node.status }}</span></td>
|
||||
<td class="p-3 text-xs">{{ node.last_seen_at_utc or '-' }}</td>
|
||||
<td class="p-3 text-xs font-mono break-all">{{ node.storage_root_path or '-' }}</td>
|
||||
<td class="p-3 text-right">
|
||||
{% if can_manage_branch_storage %}
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
{% if node.status == 'disabled_duplicate' %}
|
||||
<span class="text-xs text-slate-500">Duplicate disabled</span>
|
||||
{% else %}
|
||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/download-agent-package">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="storage_root_path" value="{{ node.storage_root_path or 'D:\AuditFirmStorage' }}">
|
||||
<button class="text-sm text-emerald-700 font-semibold">Download Agent Package</button>
|
||||
</form>
|
||||
<form method="post" action="/documents/storage-nodes/{{ node.id }}/toggle">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button class="text-sm text-blue-700 font-semibold">{{ 'Disable' if node.is_active else 'Enable' }}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}-{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr><td colspan="6" class="p-6 text-center text-slate-500">No storage nodes registered yet.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Storage Jobs</h2></div>
|
||||
<table class="w-full text-xs">
|
||||
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Job</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
|
||||
<tbody>
|
||||
{% for job in recent_jobs or [] %}
|
||||
<tr class="border-t"><td class="p-2">#{{ job.id }}</td><td class="p-2">{{ job.status }}</td><td class="p-2">{{ job.created_at_utc or '-' }}</td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent storage jobs.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="rounded-2xl border bg-white overflow-hidden shadow-sm">
|
||||
<div class="p-4 border-b"><h2 class="font-bold text-slate-900">Recent Download Requests</h2></div>
|
||||
<table class="w-full text-xs">
|
||||
<thead class="bg-slate-50 text-slate-600"><tr><th class="p-2 text-left">Request</th><th class="p-2 text-left">Status</th><th class="p-2 text-left">Created</th></tr></thead>
|
||||
<tbody>
|
||||
{% for dr in recent_download_requests or [] %}
|
||||
<tr class="border-t"><td class="p-2">#{{ dr.id }}</td><td class="p-2">{{ dr.request_status }}</td><td class="p-2">{{ dr.created_at_utc or '-' }}</td></tr>
|
||||
{% else %}
|
||||
<tr><td colspan="3" class="p-4 text-center text-slate-500">No recent download requests.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,108 @@
|
||||
{% extends "ui/templates/base/layout.html" %}
|
||||
{% block content %}
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-slate-900">Task Documents</h2>
|
||||
<p class="text-sm text-slate-500">
|
||||
{{ engagement.client.client_name if engagement.client else '-' }} ·
|
||||
{{ engagement.catalogue.service_name if engagement.catalogue else '-' }} ·
|
||||
{{ task.task_name }} · FY {{ engagement.financial_year }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<a href="/services/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement</a>
|
||||
<a href="/documents/engagements/{{ engagement.id }}" class="rounded-xl border border-slate-300 px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50">Engagement Documents</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if request.query_params.get('uploaded') %}<div class="rounded-2xl border border-emerald-200 bg-emerald-50 p-4 text-sm text-emerald-800">Task document uploaded successfully.</div>{% endif %}
|
||||
{% if request.query_params.get('error') %}<div class="rounded-2xl border border-red-200 bg-red-50 p-4 text-sm text-red-800">Upload failed. Please verify file, requirement and permission.</div>{% endif %}
|
||||
|
||||
<section class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Mandatory Pending</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ requirement_status|selectattr('is_pending_mandatory')|list|length }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Requirements</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ requirements|length }}</div></div>
|
||||
<div class="rounded-2xl bg-white p-5 shadow-soft"><div class="text-xs uppercase tracking-wide text-slate-500">Uploaded Documents</div><div class="mt-2 text-2xl font-semibold text-slate-900">{{ documents|length }}</div></div>
|
||||
</section>
|
||||
|
||||
{% if can_upload %}
|
||||
<section class="rounded-2xl bg-white p-5 shadow-soft">
|
||||
<h3 class="text-sm font-semibold text-slate-900">Upload Task Document</h3>
|
||||
<form method="post" action="/documents/tasks/{{ task.id }}/upload" enctype="multipart/form-data" class="mt-4 grid gap-4 md:grid-cols-2">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Requirement</label>
|
||||
<select name="requirement_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">General task document</option>
|
||||
{% for item in requirement_status %}
|
||||
{% set req = item.requirement %}
|
||||
<option value="{{ req.id }}">{{ req.document_name }}{% if req.is_mandatory %} (Mandatory){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Existing Document</label>
|
||||
<select name="existing_document_id" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
<option value="">Create new document</option>
|
||||
{% for doc in documents %}<option value="{{ doc.id }}">Add revision to {{ doc.title }} (v{{ doc.current_version_no }})</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Title</label>
|
||||
<input type="text" name="title" placeholder="Leave blank to use requirement name" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Document Type</label>
|
||||
<select name="document_type" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
{% for item in document_types %}<option value="{{ item }}">{{ item.replace('_',' ').title() }}</option>{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">File</label>
|
||||
<input type="file" name="file" required class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Revision Remarks</label>
|
||||
<input type="text" name="remarks" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="mb-1 block text-xs font-semibold uppercase tracking-wide text-slate-500">Description</label>
|
||||
<textarea name="description" rows="2" class="w-full rounded-xl border border-slate-300 px-3 py-2 text-sm"></textarea>
|
||||
</div>
|
||||
<div class="md:col-span-2"><button class="rounded-xl bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700">Upload</button></div>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-100 px-5 py-4"><h3 class="text-sm font-semibold text-slate-900">Requirement Status</h3></div>
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Requirement</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Type</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Status</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Documents</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for item in requirement_status %}
|
||||
{% set req = item.requirement %}
|
||||
<tr>
|
||||
<td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ req.document_name }}</div>{% if req.instructions %}<div class="text-xs text-slate-500">{{ req.instructions }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3 text-sm text-slate-700">{{ req.document_type.replace('_',' ').title() }}{% if req.allowed_file_types %}<div class="text-xs text-slate-500">{{ req.allowed_file_types }}</div>{% endif %}</td>
|
||||
<td class="px-4 py-3 text-sm">{% if item.is_uploaded %}<span class="rounded-full bg-emerald-100 px-2 py-1 text-xs text-emerald-800">Uploaded</span>{% elif req.is_mandatory %}<span class="rounded-full bg-red-100 px-2 py-1 text-xs text-red-800">Pending Mandatory</span>{% else %}<span class="rounded-full bg-slate-100 px-2 py-1 text-xs text-slate-600">Optional Pending</span>{% endif %}</td>
|
||||
<td class="px-4 py-3 text-xs text-slate-600">{% for doc in item.documents %}<div><a href="/documents/{{ doc.id }}/download" class="text-brand-700 hover:underline">{{ doc.title }}</a> · v{{ doc.current_version_no }}</div>{% else %}-{% endfor %}</td>
|
||||
</tr>
|
||||
{% else %}<tr><td colspan="4" class="px-4 py-8 text-center text-sm text-slate-500">No document requirements configured for this task template.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section class="overflow-hidden rounded-2xl bg-white shadow-soft">
|
||||
<div class="border-b border-slate-100 px-5 py-4"><h3 class="text-sm font-semibold text-slate-900">All Task Documents</h3></div>
|
||||
<table class="min-w-full divide-y divide-slate-200">
|
||||
<thead class="bg-slate-50"><tr><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Document</th><th class="px-4 py-3 text-left text-xs font-semibold uppercase text-slate-500">Latest Version</th><th class="px-4 py-3 text-right text-xs font-semibold uppercase text-slate-500">Action</th></tr></thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for doc in documents %}
|
||||
{% set latest = doc.versions[0] if doc.versions else None %}
|
||||
<tr><td class="px-4 py-3 text-sm"><div class="font-medium text-slate-900">{{ doc.title }}</div><div class="text-xs text-slate-500">{{ doc.document_type.replace('_',' ').title() }}{% if doc.document_requirement %} · {{ doc.document_requirement.document_name }}{% endif %}</div></td><td class="px-4 py-3 text-xs text-slate-600">{% if latest %}v{{ latest.version_no }} · {{ latest.original_filename }}{% else %}-{% endif %}</td><td class="px-4 py-3 text-right text-sm">{% if latest %}<a href="/documents/{{ doc.id }}/download" class="text-brand-700 hover:underline">Download</a>{% endif %}</td></tr>
|
||||
{% else %}<tr><td colspan="3" class="px-4 py-8 text-center text-sm text-slate-500">No task documents uploaded yet.</td></tr>{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user