feat: improved UX navigation and admin dashboard

- Added a unified horizontal navigation menu in the header
- Redesigned the user/logout button into a single element
- Reversed priority for extracting OIDC names (name > preferred_username)
- Admin: display documents with/without expected signatures
- Admin: detailed badges “X signatures (+Y) out of Z”
- Admin: modal for adding expected signers
- Admin: display additional signatures in stats
- Simplification of expected signers table display
- Validation pattern for document creation
- Removal of redundant links in templates
This commit is contained in:
Benjamin
2025-10-06 23:27:39 +02:00
parent 5e74921ee7
commit af3ab1f54a
12 changed files with 277 additions and 226 deletions

View File

@@ -18,6 +18,7 @@ type ExpectedSignerWithStatus struct {
ExpectedSigner
HasSigned bool `json:"has_signed"`
SignedAt *time.Time `json:"signed_at,omitempty"`
UserName *string `json:"user_name,omitempty"`
}
// DocCompletionStats provides completion statistics for a document

View File

@@ -304,20 +304,21 @@ func (s *OauthService) parseUserInfo(resp *http.Response) (*models.User, error)
}
var name string
if preferredName, ok := rawUser["preferred_username"].(string); ok && preferredName != "" {
name = preferredName
// Priority: full name first, then composite name, then username as fallback
if fullName, ok := rawUser["name"].(string); ok && fullName != "" {
name = fullName
} else if firstName, ok := rawUser["given_name"].(string); ok {
if lastName, ok := rawUser["family_name"].(string); ok {
name = firstName + " " + lastName
} else {
name = firstName
}
} else if fullName, ok := rawUser["name"].(string); ok && fullName != "" {
name = fullName
} else if cn, ok := rawUser["cn"].(string); ok && cn != "" {
name = cn
} else if displayName, ok := rawUser["display_name"].(string); ok && displayName != "" {
name = displayName
} else if preferredName, ok := rawUser["preferred_username"].(string); ok && preferredName != "" {
name = preferredName
}
user.Name = name

View File

@@ -10,8 +10,11 @@ import (
)
type DocumentAgg struct {
DocID string `json:"doc_id"`
Count int `json:"count"`
DocID string `json:"doc_id"`
Count int `json:"count"` // Total signatures
ExpectedCount int `json:"expected_count"` // Nombre de signataires attendus
SignedCount int `json:"signed_count"` // Signatures attendues signées
UnexpectedCount int `json:"unexpected_count"` // Signatures non attendues
}
// AdminRepository provides read-only access for admin operations
@@ -26,10 +29,34 @@ func NewAdminRepository(db *sql.DB) *AdminRepository {
// ListDocumentsWithCounts returns all documents with their signature counts
func (r *AdminRepository) ListDocumentsWithCounts(ctx context.Context) ([]DocumentAgg, error) {
query := `
SELECT doc_id, COUNT(*) as count
FROM signatures
GROUP BY doc_id
ORDER BY doc_id
SELECT
all_docs.doc_id,
COALESCE(sig_counts.sig_count, 0) as count,
COALESCE(expected_counts.expected_count, 0) as expected_count,
COALESCE(signed_expected.signed_count, 0) as signed_count,
COALESCE(sig_counts.sig_count, 0) - COALESCE(signed_expected.signed_count, 0) as unexpected_count
FROM (
SELECT DISTINCT doc_id FROM signatures
UNION
SELECT DISTINCT doc_id FROM expected_signers
) AS all_docs
LEFT JOIN (
SELECT doc_id, COUNT(*) as sig_count
FROM signatures
GROUP BY doc_id
) AS sig_counts USING (doc_id)
LEFT JOIN (
SELECT doc_id, COUNT(*) as expected_count
FROM expected_signers
GROUP BY doc_id
) AS expected_counts USING (doc_id)
LEFT JOIN (
SELECT es.doc_id, COUNT(DISTINCT s.id) as signed_count
FROM expected_signers es
INNER JOIN signatures s ON es.doc_id = s.doc_id AND es.email = s.user_email
GROUP BY es.doc_id
) AS signed_expected USING (doc_id)
ORDER BY all_docs.doc_id
`
rows, err := r.db.QueryContext(ctx, query)
@@ -43,7 +70,7 @@ func (r *AdminRepository) ListDocumentsWithCounts(ctx context.Context) ([]Docume
var documents []DocumentAgg
for rows.Next() {
var doc DocumentAgg
err := rows.Scan(&doc.DocID, &doc.Count)
err := rows.Scan(&doc.DocID, &doc.Count, &doc.ExpectedCount, &doc.SignedCount, &doc.UnexpectedCount)
if err != nil {
continue
}

View File

@@ -8,6 +8,7 @@ import (
"strings"
"github.com/btouchard/ackify-ce/internal/domain/models"
"github.com/btouchard/ackify-ce/pkg/logger"
)
// ExpectedSignerRepository handles database operations for expected signers
@@ -62,7 +63,12 @@ func (r *ExpectedSignerRepository) ListByDocID(ctx context.Context, docID string
if err != nil {
return nil, fmt.Errorf("failed to query expected signers: %w", err)
}
defer rows.Close()
defer func(rows *sql.Rows) {
err := rows.Close()
if err != nil {
logger.Logger.Error("failed to close rows", "error", err)
}
}(rows)
var signers []*models.ExpectedSigner
for rows.Next() {
@@ -95,7 +101,8 @@ func (r *ExpectedSignerRepository) ListWithStatusByDocID(ctx context.Context, do
es.added_by,
es.notes,
CASE WHEN s.id IS NOT NULL THEN true ELSE false END as has_signed,
s.signed_at
s.signed_at,
s.user_name
FROM expected_signers es
LEFT JOIN signatures s ON es.doc_id = s.doc_id AND es.email = s.user_email
WHERE es.doc_id = $1
@@ -106,7 +113,12 @@ func (r *ExpectedSignerRepository) ListWithStatusByDocID(ctx context.Context, do
if err != nil {
return nil, fmt.Errorf("failed to query expected signers with status: %w", err)
}
defer rows.Close()
defer func(rows *sql.Rows) {
err := rows.Close()
if err != nil {
logger.Logger.Error("failed to close rows", "error", err)
}
}(rows)
var signers []*models.ExpectedSignerWithStatus
for rows.Next() {
@@ -120,6 +132,7 @@ func (r *ExpectedSignerRepository) ListWithStatusByDocID(ctx context.Context, do
&signer.Notes,
&signer.HasSigned,
&signer.SignedAt,
&signer.UserName,
)
if err != nil {
continue

View File

@@ -4,6 +4,10 @@
"header.login": "Sign In",
"header.logout": "Sign Out",
"nav.home": "Home",
"nav.signatures": "My Signatures",
"nav.admin": "Administration",
"home.title": "Ackify",
"home.subtitle": "Professional solution to validate document reading",
"home.doc_label": "Document identifier",

View File

@@ -4,6 +4,10 @@
"header.login": "Connexion",
"header.logout": "Déconnexion",
"nav.home": "Accueil",
"nav.signatures": "Mes signatures",
"nav.admin": "Administration",
"home.title": "Ackify",
"home.subtitle": "La solution professionnelle pour valider la lecture de vos documents",
"home.doc_label": "Identifiant du document",

View File

@@ -16,7 +16,7 @@
id="newDocId"
name="doc_id"
required
pattern="[a-zA-Z0-9_-]+"
pattern="[a-zA-Z0-9\-_]+"
placeholder="{{if eq .Lang "fr"}}ex: politique-securite-2025{{else}}e.g. security-policy-2025{{end}}"
class="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
@@ -86,9 +86,17 @@
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-900">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
{{.Count}} {{if ne .Count 1}}{{index $.T "admin.signature_plural"}}{{else}}{{index $.T "admin.signature_singular"}}{{end}}
</span>
{{if gt .ExpectedCount 0}}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
{{.Count}} {{if eq .Count 1}}{{index $.T "admin.signature_singular"}}{{else}}{{index $.T "admin.signature_plural"}}{{end}}
{{if gt .UnexpectedCount 0}} (+{{.UnexpectedCount}}){{end}}
sur {{.ExpectedCount}}
</span>
{{else}}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
{{.Count}} {{if ne .Count 1}}{{index $.T "admin.signature_plural"}}{{else}}{{index $.T "admin.signature_singular"}}{{end}}
</span>
{{end}}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">

View File

@@ -25,7 +25,12 @@
</div>
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
<div class="text-sm text-green-600 font-medium">{{if eq .Lang "fr"}}Signés{{else}}Signed{{end}}</div>
<div class="text-2xl font-bold text-green-900">{{.Stats.SignedCount}}</div>
<div class="flex items-baseline justify-between">
<div class="text-2xl font-bold text-green-900">{{.Stats.SignedCount}}</div>
{{if gt (len .UnexpectedSignatures) 0}}
<div class="text-sm font-medium text-green-700">+{{len .UnexpectedSignatures}}</div>
{{end}}
</div>
</div>
<div class="bg-orange-50 border border-orange-200 rounded-lg p-4">
<div class="text-sm text-orange-600 font-medium">{{if eq .Lang "fr"}}En attente{{else}}Pending{{end}}</div>
@@ -66,39 +71,32 @@
</div>
</div>
<!-- Add Expected Signers Form -->
<div class="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
<h2 class="text-lg font-semibold text-slate-900 mb-4">
{{if eq .Lang "fr"}}Ajouter des signataires attendus{{else}}Add Expected Signers{{end}}
</h2>
<form method="POST" action="/admin/docs/{{.DocID}}/expected">
<div class="mb-4">
<label for="emails" class="block text-sm font-medium text-slate-700 mb-2">
{{if eq .Lang "fr"}}Emails (un par ligne, ou séparés par virgule/point-virgule){{else}}Emails (one per line, or separated by comma/semicolon){{end}}
</label>
<textarea name="emails" id="emails" rows="5" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent" placeholder="user1@example.com&#10;user2@example.com&#10;user3@example.com"></textarea>
</div>
<button type="submit" class="px-4 py-2 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 transition-colors">
{{if eq .Lang "fr"}}Ajouter{{else}}Add{{end}}
</button>
</form>
</div>
<!-- Expected Signers Table -->
{{if .ExpectedSigners}}
<div class="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
<h2 class="text-lg font-semibold text-slate-900 mb-4">
{{if eq .Lang "fr"}}Liste des signataires attendus{{else}}Expected Signers List{{end}}
</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-slate-900">
{{if eq .Lang "fr"}}✓ Signataires attendus{{else}}✓ Expected Signers{{end}}
{{if .ExpectedSigners}}
<span class="text-sm font-medium text-slate-600 ml-2">
({{.Stats.SignedCount}}/{{.Stats.ExpectedCount}})
</span>
{{end}}
</h2>
<button onclick="openAddSignersModal()" class="inline-flex items-center space-x-2 px-4 py-2 bg-primary-600 text-white text-sm font-medium rounded-lg hover:bg-primary-700 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
</svg>
<span>{{if eq .Lang "fr"}}Ajouter{{else}}Add{{end}}</span>
</button>
</div>
{{if .ExpectedSigners}}
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Email
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
{{if eq .Lang "fr"}}Statut{{else}}Status{{end}}
{{if eq .Lang "fr"}}Signataire{{else}}Signer{{end}}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
{{if eq .Lang "fr"}}Signé le{{else}}Signed At{{end}}
@@ -111,29 +109,34 @@
<tbody class="bg-white divide-y divide-slate-200">
{{range .ExpectedSigners}}
<tr class="hover:bg-slate-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-slate-900">{{.Email}}</div>
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
<div>
{{if .HasSigned}}
<span class="text-green-600">✓</span>
{{else}}
<span class="text-orange-500">⏳</span>
{{end}}
</div>
<div>
<div class="text-sm font-medium text-slate-900">
{{if and .UserName .HasSigned}}
{{.UserName}} &lt;{{.Email}}&gt;
{{else}}
{{.Email}}
{{end}}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{if .HasSigned}}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
{{if eq $.Lang "fr"}}Signé{{else}}Signed{{end}}
</span>
{{else}}
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
{{if eq $.Lang "fr"}}En attente{{else}}Pending{{end}}
</span>
{{end}}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
<td class="px-6 py-4 text-sm text-slate-500">
{{if .SignedAt}}
<div>{{.SignedAt.Format "02/01/2006"}}</div>
<div class="text-xs">{{.SignedAt.Format "15:04:05"}}</div>
<div>{{.SignedAt.Format "02/01 15:04"}}</div>
{{else}}
-
<span class="text-slate-400">{{if eq $.Lang "fr"}}En attente{{else}}Pending{{end}}</span>
{{end}}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm">
<td class="px-6 py-4 text-sm">
<form method="POST" action="/admin/docs/{{$.DocID}}/expected/remove" class="inline">
<input type="hidden" name="email" value="{{.Email}}">
<button type="submit" class="text-red-600 hover:text-red-900 font-medium" onclick="return confirm('{{if eq $.Lang "fr"}}Supprimer ce signataire attendu ?{{else}}Remove this expected signer?{{end}}')">
@@ -146,15 +149,24 @@
</tbody>
</table>
</div>
{{else}}
<div class="text-center py-8 text-slate-500">
<p>{{if eq .Lang "fr"}}Aucun signataire attendu pour le moment{{else}}No expected signers yet{{end}}</p>
</div>
{{end}}
</div>
{{end}}
<!-- Unexpected Signatures Section -->
<!-- Unexpected Signatures Table -->
{{if .UnexpectedSignatures}}
<div class="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
<h2 class="text-lg font-semibold text-slate-900 mb-4">
{{if eq .Lang "fr"}}Signatures non attendues{{else}}Unexpected Signatures{{end}}
</h2>
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-slate-900">
{{if eq .Lang "fr"}}⚠ Signatures non attendues{{else}}⚠ Unexpected Signatures{{end}}
</h2>
<span class="text-sm font-medium text-slate-600">
{{len .UnexpectedSignatures}}
</span>
</div>
<p class="text-sm text-slate-600 mb-4">
{{if eq .Lang "fr"}}Ces utilisateurs ont signé mais n'étaient pas dans la liste des signataires attendus.{{else}}These users signed but were not in the expected signers list.{{end}}
</p>
@@ -163,7 +175,7 @@
<thead class="bg-slate-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Email
{{if eq .Lang "fr"}}Signataire{{else}}Signer{{end}}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
{{if eq .Lang "fr"}}Signé le{{else}}Signed At{{end}}
@@ -173,15 +185,24 @@
<tbody class="bg-white divide-y divide-slate-200">
{{range .UnexpectedSignatures}}
<tr class="hover:bg-slate-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm font-medium text-slate-900">{{.UserEmail}}</div>
{{if .UserName}}
<div class="text-sm text-slate-500">{{.UserName}}</div>
{{end}}
<td class="px-6 py-4">
<div class="flex items-center space-x-2">
<div>
<span class="text-green-600">✓</span>
</div>
<div>
<div class="text-sm font-medium text-slate-900">
{{if .UserName}}
{{.UserName}} &lt;{{.UserEmail}}&gt;
{{else}}
{{.UserEmail}}
{{end}}
</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
<div>{{.SignedAtUTC.Format "02/01/2006"}}</div>
<div class="text-xs">{{.SignedAtUTC.Format "15:04:05"}}</div>
<td class="px-6 py-4 text-sm text-slate-500">
<div>{{.SignedAtUTC.Format "02/01 15:04"}}</div>
</td>
</tr>
{{end}}
@@ -191,68 +212,8 @@
</div>
{{end}}
<!-- All Signatures Section -->
{{if .Signatures}}
<div class="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
<h2 class="text-lg font-semibold text-slate-900 mb-4">
{{if eq .Lang "fr"}}Toutes les signatures{{else}}All Signatures{{end}} ({{len .Signatures}})
</h2>
<div class="overflow-hidden">
<table class="min-w-full divide-y divide-slate-200">
<thead class="bg-slate-50">
<tr>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
{{if eq .Lang "fr"}}Utilisateur{{else}}User{{end}}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
{{if eq .Lang "fr"}}Signé le{{else}}Signed At{{end}}
</th>
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase tracking-wider">
Service
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-slate-200">
{{range .Signatures}}
<tr class="hover:bg-slate-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="w-8 h-8 bg-primary-100 rounded-full flex items-center justify-center">
<svg class="w-4 h-4 text-primary-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
</div>
<div class="ml-3">
{{if .UserName}}
<div class="text-sm font-medium text-slate-900">{{.UserName}}</div>
{{end}}
<div class="text-sm text-slate-500">{{.UserEmail}}</div>
</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<div class="text-sm text-slate-900">{{.SignedAtUTC.Format "02/01/2006"}}</div>
<div class="text-sm text-slate-500">{{.SignedAtUTC.Format "15:04:05"}}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
{{$serviceInfo := .GetServiceInfo}}
{{if $serviceInfo}}
<div class="flex items-center">
<img src="{{$serviceInfo.IconURL}}" alt="{{$serviceInfo.Name}}" class="w-4 h-4 mr-2">
<span class="text-sm text-slate-900">{{$serviceInfo.Name}}</span>
</div>
{{else}}
<span class="text-sm text-slate-500">-</span>
{{end}}
</td>
</tr>
{{end}}
</tbody>
</table>
</div>
</div>
<!-- Chain Integrity Section -->
{{if .Signatures}}
{{if .ChainIntegrity}}
{{if .ChainIntegrity.IsValid}}
<div class="bg-green-50 border border-green-200 rounded-lg p-4">
@@ -296,25 +257,54 @@
</div>
{{end}}
{{end}}
{{else}}
<div class="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
<div class="text-center py-12">
<div class="w-16 h-16 mx-auto mb-4 bg-slate-100 rounded-full flex items-center justify-center">
<svg class="w-8 h-8 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<h3 class="text-lg font-medium text-slate-900 mb-2">
{{if eq .Lang "fr"}}Aucune signature pour le moment{{else}}No signatures yet{{end}}
</h3>
<p class="text-slate-600">
{{if eq .Lang "fr"}}Ce document n'a pas encore été signé.{{else}}This document has not been signed yet.{{end}}
</p>
</div>
</div>
{{end}}
</div>
<!-- Add Signers Modal -->
<div id="addSignersModal" class="hidden fixed inset-0 bg-black bg-opacity-50 z-50 flex items-center justify-center p-4">
<div class="bg-white rounded-lg shadow-xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex items-center justify-between mb-4">
<h3 class="text-xl font-semibold text-slate-900">
{{if eq .Lang "fr"}}Ajouter des signataires attendus{{else}}Add Expected Signers{{end}}
</h3>
<button onclick="closeAddSignersModal()" class="text-slate-400 hover:text-slate-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<form method="POST" action="/admin/docs/{{.DocID}}/expected">
<div class="mb-4">
<label for="modalEmails" class="block text-sm font-medium text-slate-700 mb-2">
{{if eq .Lang "fr"}}Emails (un par ligne, ou séparés par virgule/point-virgule){{else}}Emails (one per line, or separated by comma/semicolon){{end}}
</label>
<textarea
name="emails"
id="modalEmails"
rows="8"
class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
placeholder="user1@example.com&#10;user2@example.com&#10;user3@example.com"
></textarea>
<p class="mt-2 text-xs text-slate-500">
{{if eq .Lang "fr"}}Vous pouvez coller une liste d'emails séparés par des sauts de ligne, virgules ou point-virgules{{else}}You can paste a list of emails separated by newlines, commas or semicolons{{end}}
</p>
</div>
<div class="flex justify-end space-x-3">
<button type="button" onclick="closeAddSignersModal()" class="px-4 py-2 border border-slate-300 text-slate-700 font-medium rounded-lg hover:bg-slate-50 transition-colors">
{{if eq .Lang "fr"}}Annuler{{else}}Cancel{{end}}
</button>
<button type="submit" class="px-4 py-2 bg-primary-600 text-white font-medium rounded-lg hover:bg-primary-700 transition-colors">
{{if eq .Lang "fr"}}Ajouter{{else}}Add{{end}}
</button>
</div>
</form>
</div>
</div>
</div>
<script>
function copyShareLink() {
const linkInput = document.getElementById('shareLink');
@@ -333,5 +323,29 @@ function copyShareLink() {
console.error('Failed to copy:', err);
}
}
function openAddSignersModal() {
document.getElementById('addSignersModal').classList.remove('hidden');
document.getElementById('modalEmails').focus();
}
function closeAddSignersModal() {
document.getElementById('addSignersModal').classList.add('hidden');
document.getElementById('modalEmails').value = '';
}
// Close modal on Escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeAddSignersModal();
}
});
// Close modal on backdrop click
document.getElementById('addSignersModal').addEventListener('click', function(e) {
if (e.target === this) {
closeAddSignersModal();
}
});
</script>
{{end}}

View File

@@ -24,6 +24,7 @@
<h1 class="text-xl font-bold text-slate-900">{{index .T "site.brand"}}</h1>
</div>
</a>
<div class="flex items-center space-x-4">
<!-- Language Switcher -->
<div class="flex items-center space-x-1">
@@ -41,17 +42,12 @@
}
</script>
{{if .User}}
<div class="text-sm text-slate-600">
<span class="inline-flex items-center space-x-2">
<div class="w-6 h-6 bg-primary-100 rounded-full flex items-center justify-center">
<svg class="w-3 h-3 text-primary-600" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
</svg>
</div>
<span>{{if .User.Name}}{{.User.Name}}{{else}}{{.User.Email}}{{end}}</span>
</span>
</div>
<a href="/logout" onclick="localStorage.setItem('ackify_silent_login_attempted', Date.now().toString());" class="text-sm text-slate-500 hover:text-slate-700 underline">{{index .T "header.logout"}}</a>
<a href="/logout" onclick="localStorage.setItem('ackify_silent_login_attempted', Date.now().toString());" class="inline-flex items-center space-x-2 px-4 py-2 bg-white border border-slate-300 hover:border-red-500 hover:bg-red-50 text-slate-700 hover:text-red-700 text-sm font-medium rounded-xl transition-all duration-200 shadow-sm hover:shadow">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
</svg>
<span>{{if .User.Name}}{{.User.Name}}{{else}}{{.User.Email}}{{end}}</span>
</a>
{{else}}
<a href="/login" class="inline-flex items-center space-x-2 px-4 py-2 bg-white border border-slate-300 hover:border-primary-500 hover:bg-primary-50 text-slate-700 hover:text-primary-700 text-sm font-medium rounded-xl transition-all duration-200 shadow-sm hover:shadow">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -63,8 +59,39 @@
</div>
</div>
</div>
<!-- Navigation Menu (visible when logged in) -->
{{if .User}}
<div class="border-t border-slate-200 bg-white/60">
<div class="max-w-4xl mx-auto px-6">
<nav class="flex items-center space-x-1 py-2">
<a href="/" class="inline-flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"/>
</svg>
<span>{{index .T "nav.home"}}</span>
</a>
<a href="/signatures" class="inline-flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{index .T "nav.signatures"}}</span>
</a>
{{if .IsAdmin}}
<a href="/admin" class="inline-flex items-center space-x-2 px-4 py-2 text-sm font-medium text-slate-700 hover:text-primary-700 hover:bg-primary-50 rounded-lg transition-all">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>{{index .T "nav.admin"}}</span>
</a>
{{end}}
</nav>
</div>
</div>
{{end}}
</header>
<main class="flex-1 py-8">
<div class="max-w-4xl mx-auto px-6">
{{if eq .TemplateName "sign"}}

View File

@@ -19,30 +19,9 @@
<div class="px-8 py-8">
<form method="GET" action="/sign" class="space-y-6">
<div>
<div class="flex justify-between items-center mb-3">
<label for="doc" class="text-sm font-semibold text-slate-700">
{{index .T "home.doc_label"}}
</label>
{{if .User}}
<div class="flex items-center space-x-4">
<a href="/signatures" class="text-sm font-medium text-primary-600 hover:text-primary-700 transition-colors flex items-center space-x-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{index .T "home.my_signatures"}}</span>
</a>
{{if .IsAdmin}}
<a href="/admin" class="text-sm font-medium text-orange-600 hover:text-orange-700 transition-colors flex items-center space-x-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
<span>{{index .T "home.administration"}}</span>
</a>
{{end}}
</div>
{{end}}
</div>
<label for="doc" class="block text-sm font-semibold text-slate-700 mb-3">
{{index .T "home.doc_label"}}
</label>
<div class="relative">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<svg class="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -90,25 +90,5 @@
{{end}}
</div>
</div>
<!-- Actions Card -->
<div class="bg-white rounded-2xl border border-slate-200 p-6">
<h4 class="font-semibold text-slate-900 mb-4">{{index .T "sign.actions_title"}}</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<a href="/signatures" target="_blank" class="flex items-center justify-center space-x-2 px-4 py-3 bg-slate-50 hover:bg-slate-100 text-slate-700 hover:text-slate-900 rounded-xl transition-all duration-200 text-sm font-medium text-center border border-slate-200 hover:border-slate-300">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>{{index .T "sign.view_signatures"}}</span>
</a>
<a href="/" class="flex items-center justify-center space-x-2 px-4 py-3 bg-primary-50 hover:bg-primary-100 text-primary-700 hover:text-primary-800 rounded-xl transition-all duration-200 text-sm font-medium text-center border border-primary-200 hover:border-primary-300">
<svg class="w-4 h-4 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9l9-7 9 7v11a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
</svg>
<span>{{index .T "sign.back_home"}}</span>
</a>
</div>
</div>
</div>
{{end}}

View File

@@ -3,23 +3,16 @@
<!-- Header -->
<div class="bg-white rounded-3xl shadow-xl border border-slate-200 overflow-hidden">
<div class="bg-gradient-to-r from-primary-600 to-primary-700 px-8 py-6">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div>
<h2 class="text-2xl font-bold text-white">{{index .T "signatures.title"}}</h2>
<p class="text-primary-100">{{index .T "signatures.subtitle"}}</p>
</div>
</div>
<a href="/" class="text-primary-100 hover:text-white transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
<div class="flex items-center space-x-4">
<div class="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center">
<svg class="w-7 h-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</a>
</div>
<div>
<h2 class="text-2xl font-bold text-white">{{index .T "signatures.title"}}</h2>
<p class="text-primary-100">{{index .T "signatures.subtitle"}}</p>
</div>
</div>
</div>
</div>