mirror of
https://github.com/btouchard/ackify-ce.git
synced 2026-02-08 23:08:58 -06:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 user2@example.com 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}} <{{.Email}}>
|
||||
{{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}} <{{.UserEmail}}>
|
||||
{{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 user2@example.com 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}}
|
||||
|
||||
@@ -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"}}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user