This commit is contained in:
2026-07-28 02:04:36 +03:30
commit a5aed05b98
31 changed files with 3500 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
{{define "admin.html"}}
{{template "shell-start" .}}
<header class="page-head">
<div><p class="eyebrow">ADMIN</p><h1>Team approvals</h1><p>Review time-off and remote-day requests.</p></div>
<div class="filter-tabs"><a href="/admin/requests" class="active">All</a><a href="/admin/requests?status=pending">Pending</a><a href="/admin/requests?status=approved">Approved</a></div>
</header>
{{template "notice" .}}
<section class="card approval-card">
{{if .Requests}}
<div class="request-list">
{{range .Requests}}
<article class="approval-row">
<span class="avatar small">{{initial .UserName}}</span>
<div class="approval-main">
<div class="approval-title"><strong>{{.UserName}}</strong><span class="badge kind">{{kindLabel .Kind}}</span><span class="badge {{.Status}}">{{statusLabel .Status}}</span></div>
<p>{{dateFA .StartDate}}{{if ne .StartDate .EndDate}} — {{dateFA .EndDate}}{{end}} · {{.DayCount}} day(s)</p>
{{if .Reason}}<blockquote>“{{.Reason}}”</blockquote>{{end}}
{{if eq .Status "pending"}}
<form method="post" action="/admin/requests/{{.ID}}/review" class="review-form">
<input type="hidden" name="csrf" value="{{$.CSRF}}">
<input name="note" maxlength="500" placeholder="Optional note to teammate">
<button class="button approve" name="decision" value="approved">Approve</button>
<button class="button reject" name="decision" value="rejected">Reject</button>
</form>
{{else if .AdminNote}}<p class="admin-note">Review note: {{.AdminNote}}</p>{{end}}
</div>
</article>
{{end}}
</div>
{{else}}<div class="empty roomy"><span></span><h3>All clear</h3><p>No requests match this view.</p></div>{{end}}
</section>
{{template "shell-end" .}}
{{end}}
+84
View File
@@ -0,0 +1,84 @@
{{define "head"}}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="color-scheme" content="light">
<title>{{.Title}} · Hamkar</title>
<script src="/static/theme.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
<link rel="stylesheet" href="/static/style.css">
<script src="/static/jalali-picker.js" defer></script>
<script src="https://unpkg.com/htmx.org@2.0.4" defer></script>
</head>
<body>
{{end}}
{{define "theme-toggle"}}
<button type="button" class="theme-toggle" data-theme-toggle aria-label="Switch to dark mode" title="Change color mode">
<span class="theme-sun" aria-hidden="true"></span>
<span class="theme-moon" aria-hidden="true"></span>
</button>
{{end}}
{{define "sidebar"}}
<aside class="sidebar">
<a class="brand" href="/">
<span class="brand-mark">ه</span>
<span>Hamkar<small>Team presence</small></span>
</a>
<nav>
<a href="/" class="{{if eq .SelectedSection "dashboard"}}active{{end}}">
<span class="nav-icon"></span> Overview
</a>
<a href="/day" class="{{if eq .SelectedSection "day"}}active{{end}}">
<span class="nav-icon"></span> Team day
</a>
<a href="/requests" class="{{if eq .SelectedSection "requests"}}active{{end}}">
<span class="nav-icon"></span> My requests
</a>
{{if eq .User.Role "admin"}}
<p class="nav-label">ADMIN</p>
<a href="/admin/requests" class="{{if eq .SelectedSection "admin"}}active{{end}}">
<span class="nav-icon"></span> Approvals
{{if .PendingCount}}<span class="nav-count">{{.PendingCount}}</span>{{end}}
</a>
<a href="/admin/users" class="{{if eq .SelectedSection "users"}}active{{end}}">
<span class="nav-icon"></span> Teammates
</a>
<a href="/reports" class="{{if eq .SelectedSection "reports"}}active{{end}}">
<span class="nav-icon"></span> Reports
</a>
{{end}}
</nav>
<div class="sidebar-theme">{{template "theme-toggle" .}}<span>Appearance</span></div>
<div class="sidebar-user">
<span class="avatar">{{initial .User.DisplayName}}</span>
<span><strong>{{.User.DisplayName}}</strong><small>{{.User.Role}}</small></span>
<form method="post" action="/logout">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="icon-button" title="Sign out" aria-label="Sign out"></button>
</form>
</div>
</aside>
{{end}}
{{define "shell-start"}}
{{template "head" .}}
<div class="app-shell">
{{template "sidebar" .}}
<main class="main">
{{end}}
{{define "shell-end"}}
</main>
</div>
</body>
</html>
{{end}}
{{define "notice"}}
{{if .Flash}}<div class="notice success">{{.Flash}}</div>{{end}}
{{if .Error}}<div class="notice error">{{.Error}}</div>{{end}}
{{end}}
+35
View File
@@ -0,0 +1,35 @@
{{define "calendar.html"}}
<section id="calendar-panel" class="card calendar-card">
<header class="calendar-head">
<div>
<p class="eyebrow">PERSIAN CALENDAR</p>
<h2>{{.Calendar.MonthName}} <span>{{.Calendar.Year}}</span></h2>
<p class="fa-month" lang="fa" dir="rtl">{{.Calendar.MonthNameFA}} {{.Calendar.Year}}</p>
</div>
<div class="calendar-nav">
<a aria-label="Previous month" href="/?month={{.Calendar.Prev}}" hx-get="/calendar?month={{.Calendar.Prev}}" hx-target="#calendar-panel" hx-swap="outerHTML"></a>
<a aria-label="Next month" href="/?month={{.Calendar.Next}}" hx-get="/calendar?month={{.Calendar.Next}}" hx-target="#calendar-panel" hx-swap="outerHTML"></a>
</div>
</header>
<div class="calendar-grid weekdays">
<span>Sat</span><span>Sun</span><span>Mon</span><span>Tue</span><span>Wed</span><span>Thu</span><span>Fri</span>
</div>
<div class="calendar-grid days">
{{range .Calendar.Cells}}
{{if .InMonth}}
<a href="/day?date={{.Gregorian}}" class="day {{if .IsToday}}today{{end}} {{if .IsFriday}}friday{{end}} {{if .Holiday}}holiday{{end}}" title="View team details for this day">
<span class="day-number">{{.Day}}</span>
{{if .Status}}<i class="status-dot {{.Status}}" title="{{.Status}}"></i>{{end}}
{{if .Holiday}}<small title="{{.Holiday}}">{{.Holiday}}</small>{{end}}
</a>
{{else}}<div class="day blank"></div>{{end}}
{{end}}
</div>
<footer class="calendar-legend">
<span><i class="status-dot office"></i> Office</span>
<span><i class="status-dot remote"></i> Remote</span>
<span><i class="status-dot leave"></i> Time off</span>
<span><i class="holiday-mark"></i> Holiday</span>
</footer>
</section>
{{end}}
+63
View File
@@ -0,0 +1,63 @@
{{define "dashboard.html"}}
{{template "shell-start" .}}
<header class="page-head">
<div><p class="eyebrow">TEAM WORKSPACE</p><h1>Good day, {{.User.DisplayName}}</h1><p>Heres your month at a glance.</p></div>
<div class="today-pill"><span>Today</span><strong>{{.TodayJalali}}</strong></div>
</header>
{{template "notice" .}}
<div class="dashboard-grid">
<section class="card presence-card">
<div class="presence-top">
<div><p class="eyebrow">TODAYS PRESENCE</p><h2>{{if .Attendance}}{{if .Attendance.CheckOut.Valid}}Day complete{{else}}Youre checked in{{end}}{{else}}Ready when you are{{end}}</h2></div>
<span class="live-badge"><i></i> {{if and .Attendance (not .Attendance.CheckOut.Valid)}}ACTIVE{{else}}TODAY{{end}}</span>
</div>
{{if .Attendance}}
<div class="time-line">
<div><small>CHECKED IN</small><strong>{{timeHM .Attendance.CheckIn}}</strong></div>
<span></span>
<div><small>CHECKED OUT</small><strong>{{timeHM .Attendance.CheckOut}}</strong></div>
<div><small>LOCATION</small><strong>{{if eq .Attendance.Mode "remote"}}Remote{{else}}Office{{end}}</strong></div>
</div>
{{if not .Attendance.CheckOut.Valid}}
<form method="post" action="/attendance/check-out">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<button class="button dark full">Check out</button>
</form>
{{end}}
{{else}}
<p class="muted">Start your day by choosing where youre working.</p>
<div class="checkin-actions">
<form method="post" action="/attendance/check-in">
<input type="hidden" name="csrf" value="{{.CSRF}}"><input type="hidden" name="mode" value="office">
<button class="button primary">Check in at office</button>
</form>
<form method="post" action="/attendance/check-in">
<input type="hidden" name="csrf" value="{{.CSRF}}"><input type="hidden" name="mode" value="remote">
<button class="button secondary">Working remotely</button>
</form>
</div>
{{end}}
</section>
<section class="stats-grid">
<div class="stat card"><span class="stat-icon mint"></span><small>PRESENT DAYS</small><strong>{{.Stats.Present}}</strong><p>This month</p></div>
<div class="stat card"><span class="stat-icon blue"></span><small>REMOTE DAYS</small><strong>{{.Stats.Remote}}</strong><p>This month</p></div>
<div class="stat card"><span class="stat-icon sand"></span><small>TIME OFF</small><strong>{{.Stats.Leave}}</strong><p>Approved days</p></div>
</section>
{{template "calendar.html" .}}
<section class="card recent-card">
<header class="section-head"><div><p class="eyebrow">RECENT ACTIVITY</p><h2>Your requests</h2></div><a href="/requests">View all →</a></header>
{{if .Requests}}
<div class="request-list compact">
{{range .Requests}}
<div class="request-row">
<span class="request-icon {{.Kind}}">{{if eq .Kind "remote"}}⌂{{else}}☼{{end}}</span>
<div><strong>{{kindLabel .Kind}}</strong><small>{{dateFA .StartDate}}{{if ne .StartDate .EndDate}} — {{dateFA .EndDate}}{{end}}</small></div>
<span class="badge {{.Status}}">{{statusLabel .Status}}</span>
</div>
{{end}}
</div>
{{else}}<div class="empty"><span></span><p>No requests yet.</p><a href="/requests">Make a request</a></div>{{end}}
</section>
</div>
{{template "shell-end" .}}
{{end}}
+56
View File
@@ -0,0 +1,56 @@
{{define "day.html"}}
{{template "shell-start" .}}
<header class="page-head day-page-head">
<div><p class="eyebrow">TEAM DAY</p><h1>Whos working?</h1><p>See the teams presence, remote work, and absences for one day.</p></div>
<form method="get" action="/day" class="day-date-form">
<label>Persian date
<span class="jalali-field">
<input name="date" value="{{.Day.Jalali}}" data-jalali-picker required autocomplete="off" inputmode="numeric" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}">
<button type="button" class="jalali-trigger" aria-label="Choose team date" title="Open Persian calendar"></button>
</span>
</label>
<button class="button primary" type="submit">View day</button>
</form>
</header>
{{template "notice" .}}
<div class="day-layout">
<section class="card day-summary-card">
<div class="day-date-nav">
<a href="/day?date={{.Day.Prev}}" aria-label="Previous day"></a>
<div>
<p class="eyebrow">PERSIAN CALENDAR</p>
<h2>{{.Day.Jalali}}</h2>
<span>{{.Day.Weekday}} · {{.Day.Gregorian}}{{if .Day.DayNote}} · {{.Day.DayNote}}{{end}}</span>
</div>
<a href="/day?date={{.Day.Next}}" aria-label="Next day"></a>
</div>
<div class="day-stats">
<div><i class="presence-mark present"></i><span><strong>{{.Day.Present}}</strong><small>Present</small></span></div>
<div><i class="presence-mark remote"></i><span><strong>{{.Day.Remote}}</strong><small>Remote</small></span></div>
<div><i class="presence-mark absent"></i><span><strong>{{.Day.Absent}}</strong><small>Absent</small></span></div>
</div>
</section>
<section class="card roster-card">
<header class="section-head">
<div><p class="eyebrow">TEAM ROSTER</p><h2>{{len .Day.Members}} teammates</h2></div>
<div class="roster-legend"><span><i class="presence-mark present"></i> Present</span><span><i class="presence-mark remote"></i> Remote</span><span><i class="presence-mark absent"></i> Absent</span></div>
</header>
{{if .Day.Members}}
<div class="roster-list">
{{range .Day.Members}}
<article class="roster-row">
{{if .User.AvatarURL}}<img class="avatar" src="{{.User.AvatarURL}}" alt="">{{else}}<span class="avatar">{{initial .User.DisplayName}}</span>{{end}}
<div class="roster-person"><strong>{{.User.DisplayName}}</strong><small>@{{.User.Username}}</small></div>
<div class="roster-detail"><span>{{.Detail}}</span>{{if .CheckIn}}<small>{{if .CheckOut}}Completed{{else}}Active{{end}}</small>{{end}}</div>
<span class="presence-badge {{.Status}}"><i class="presence-mark {{.Status}}"></i>{{.Label}}</span>
</article>
{{end}}
</div>
{{else}}
<div class="empty roomy"><span></span><h3>No teammates yet</h3><p>Add teammates to see the daily roster.</p></div>
{{end}}
</section>
</div>
{{template "shell-end" .}}
{{end}}
+44
View File
@@ -0,0 +1,44 @@
{{define "login.html"}}
{{template "head" .}}
<main class="login-page">
<div class="auth-theme">{{template "theme-toggle" .}}</div>
<section class="login-story">
<a class="brand light" href="/">
<span class="brand-mark">ه</span>
<span>Hamkar<small>Team presence</small></span>
</a>
<div class="story-copy">
<p class="eyebrow">A lighter way to work together</p>
<h1>Know whos here.<br>Plan whats next.</h1>
<p>Presence, time off, and remote days—organized around the Persian calendar your team actually uses.</p>
</div>
<div class="story-calendar" aria-hidden="true">
<div><small>امروز</small><strong>۶ مرداد</strong><span>۱۴۰۵</span></div>
<div class="people-dots"><i></i><i></i><i></i><b>+8</b></div>
</div>
</section>
<section class="login-form-wrap">
<form class="login-card" method="post" action="/login">
<div class="mobile-brand"><span class="brand-mark">ه</span> Hamkar</div>
<p class="eyebrow">WELCOME BACK</p>
<h2>Sign in to your workspace</h2>
<p class="muted">Use your email, username, or company SSO.</p>
{{if .Error}}<div class="notice error">{{.Error}}</div>{{end}}
<label>Email or username<input name="identifier" autocomplete="username" required autofocus placeholder="you@example.com or navid"></label>
<label>Password<input type="password" name="password" autocomplete="current-password" required placeholder="••••••••"></label>
<button class="button primary full" type="submit">Sign in</button>
{{if or .OAuthGitHub .OAuthGoogle}}
<div class="separator"><span>or continue with</span></div>
<div class="oauth-row">
{{if .OAuthGoogle}}<a class="button secondary" href="/auth/google">Google</a>{{end}}
{{if .OAuthGitHub}}<a class="button secondary" href="/auth/github">GitHub</a>{{end}}
</div>
{{end}}
<p class="auth-switch">New to Hamkar? <a href="/register">Create an account</a></p>
<p class="login-hint">Demo admin: <code>admin</code> / <code>admin123</code></p>
</form>
</section>
</main>
</body>
</html>
{{end}}
+51
View File
@@ -0,0 +1,51 @@
{{define "register.html"}}
{{template "head" .}}
<main class="login-page">
<div class="auth-theme">{{template "theme-toggle" .}}</div>
<section class="login-story register-story">
<a class="brand light" href="/">
<span class="brand-mark">ه</span>
<span>Hamkar<small>Team presence</small></span>
</a>
<div class="story-copy">
<p class="eyebrow">Your team, in one place</p>
<h1>Start showing up.<br>Stay in sync.</h1>
<p>Create an account with email or username and password—or continue with your preferred SSO provider.</p>
</div>
<div class="story-calendar" aria-hidden="true">
<div><small>شروع همکاری</small><strong>حساب جدید</strong><span>خوش آمدید</span></div>
<div class="people-dots"><i></i><i></i><i></i><b>+</b></div>
</div>
</section>
<section class="login-form-wrap register-wrap">
<form class="login-card register-card" method="post" action="/register">
<div class="mobile-brand"><span class="brand-mark">ه</span> Hamkar</div>
<p class="eyebrow">JOIN YOUR TEAM</p>
<h2>Create your account</h2>
<p class="muted">Use an email or username, plus a secure password.</p>
{{if .Error}}<div class="notice error">{{.Error}}</div>{{end}}
<label>Full name<input name="display_name" autocomplete="name" required autofocus maxlength="100" placeholder="Sara Ahmadi"></label>
<div class="form-row">
<label>Email <small>Optional</small><input type="email" name="email" autocomplete="email" maxlength="200" placeholder="sara@example.com"></label>
<label>Username <small>Optional</small><input name="username" autocomplete="username" maxlength="40" placeholder="sara"></label>
</div>
<p class="field-help">Enter at least an email address or username.</p>
<div class="form-row">
<label>Password<input type="password" name="password" autocomplete="new-password" minlength="8" required placeholder="At least 8 characters"></label>
<label>Confirm password<input type="password" name="password_confirm" autocomplete="new-password" minlength="8" required placeholder="Repeat password"></label>
</div>
<button class="button primary full" type="submit">Create account</button>
{{if or .OAuthGitHub .OAuthGoogle}}
<div class="separator"><span>or sign up with SSO</span></div>
<div class="oauth-row">
{{if .OAuthGoogle}}<a class="button secondary" href="/auth/google">Google</a>{{end}}
{{if .OAuthGitHub}}<a class="button secondary" href="/auth/github">GitHub</a>{{end}}
</div>
{{end}}
<p class="auth-switch">Already have an account? <a href="/login">Sign in</a></p>
</form>
</section>
</main>
</body>
</html>
{{end}}
+14
View File
@@ -0,0 +1,14 @@
{{define "reports.html"}}
{{template "shell-start" .}}
<header class="page-head"><div><p class="eyebrow">ADMIN</p><h1>Reports</h1><p>Export team presence data for payroll or analysis.</p></div></header>
<section class="card report-card">
<div class="report-illustration"></div>
<div><p class="eyebrow">ATTENDANCE EXPORT</p><h2>Download presence records</h2><p class="muted">The UTF-8 CSV includes Gregorian and Persian dates, check-in/out time, location, and approved requests. It opens directly in Excel.</p></div>
<form method="get" action="/reports/attendance.csv" class="report-form">
<label>From<input type="date" name="start" value="{{.ReportStart}}" required></label>
<label>To<input type="date" name="end" value="{{.ReportEnd}}" required></label>
<button class="button primary">Download CSV</button>
</form>
</section>
{{template "shell-end" .}}
{{end}}
+42
View File
@@ -0,0 +1,42 @@
{{define "requests.html"}}
{{template "shell-start" .}}
<header class="page-head">
<div><p class="eyebrow">PLANNING</p><h1>My requests</h1><p>Ask for time off or a remote work day.</p></div>
</header>
{{template "notice" .}}
<div class="two-column">
<section class="card form-card">
<p class="eyebrow">NEW REQUEST</p><h2>Plan a day away</h2>
<form method="post" action="/requests" class="stack-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<fieldset class="choice-cards">
<label><input type="radio" name="kind" value="leave" checked><span><b></b><strong>Time off</strong><small>Holiday or personal leave</small></span></label>
<label><input type="radio" name="kind" value="remote"><span><b></b><strong>Remote day</strong><small>Work away from the office</small></span></label>
</fieldset>
<div class="form-row">
<label>From <small>Persian date</small><span class="jalali-field"><input name="start_date" data-jalali-picker data-jalali-role="start" required autocomplete="off" inputmode="numeric" placeholder="1405-05-06" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"><button type="button" class="jalali-trigger" aria-label="Choose start date" title="Open Persian calendar"></button></span></label>
<label>To <small>Persian date</small><span class="jalali-field"><input name="end_date" data-jalali-picker data-jalali-role="end" required autocomplete="off" inputmode="numeric" placeholder="1405-05-06" pattern="[0-9]{4}-[0-9]{2}-[0-9]{2}"><button type="button" class="jalali-trigger" aria-label="Choose end date" title="Open Persian calendar"></button></span></label>
</div>
<label>Note <small>Optional</small><textarea name="reason" rows="4" maxlength="500" placeholder="Anything your manager should know?"></textarea></label>
<button class="button primary full">Send for approval</button>
</form>
</section>
<section class="card">
<header class="section-head"><div><p class="eyebrow">HISTORY</p><h2>All requests</h2></div></header>
{{if .Requests}}
<div class="request-list">
{{range .Requests}}
<div class="request-row request-detail">
<span class="request-icon {{.Kind}}">{{if eq .Kind "remote"}}⌂{{else}}☼{{end}}</span>
<div><strong>{{kindLabel .Kind}}</strong><small>{{dateFA .StartDate}}{{if ne .StartDate .EndDate}} — {{dateFA .EndDate}}{{end}} · {{.DayCount}} day(s)</small>{{if .Reason}}<p>{{.Reason}}</p>{{end}}{{if .AdminNote}}<p class="admin-note">Manager: {{.AdminNote}}</p>{{end}}</div>
<div class="request-end"><span class="badge {{.Status}}">{{statusLabel .Status}}</span>
{{if eq .Status "pending"}}<form method="post" action="/requests/{{.ID}}/cancel"><input type="hidden" name="csrf" value="{{$.CSRF}}"><button class="text-button">Cancel</button></form>{{end}}
</div>
</div>
{{end}}
</div>
{{else}}<div class="empty"><span></span><p>You havent made any requests.</p></div>{{end}}
</section>
</div>
{{template "shell-end" .}}
{{end}}
+38
View File
@@ -0,0 +1,38 @@
{{define "users.html"}}
{{template "shell-start" .}}
<header class="page-head">
<div><p class="eyebrow">ADMIN</p><h1>Teammates</h1><p>Create local accounts and see who has access.</p></div>
</header>
{{template "notice" .}}
<div class="two-column users-layout">
<section class="card form-card">
<p class="eyebrow">NEW ACCOUNT</p><h2>Add a teammate</h2>
<form method="post" action="/admin/users" class="stack-form">
<input type="hidden" name="csrf" value="{{.CSRF}}">
<label>Full name<input name="display_name" required maxlength="100" placeholder="Sara Ahmadi"></label>
<div class="form-row">
<label>Username<input name="username" required maxlength="40" autocomplete="off" placeholder="sara"></label>
<label>Email <small>Optional</small><input type="email" name="email" maxlength="200" placeholder="sara@example.com"></label>
</div>
<label>Temporary password<input type="password" name="password" minlength="8" required autocomplete="new-password" placeholder="At least 8 characters"></label>
<label>Access level
<select name="role"><option value="member">Member</option><option value="admin">Admin</option></select>
</label>
<button class="button primary full">Create account</button>
</form>
</section>
<section class="card">
<header class="section-head"><div><p class="eyebrow">DIRECTORY</p><h2>{{len .Users}} teammates</h2></div></header>
<div class="user-list">
{{range .Users}}
<div class="user-row">
<span class="avatar">{{initial .DisplayName}}</span>
<div><strong>{{.DisplayName}}</strong><small>@{{.Username}}{{if .Email}} · {{.Email}}{{end}}</small></div>
<span class="badge {{if eq .Role "admin"}}approved{{else}}kind{{end}}">{{.Role}}</span>
</div>
{{end}}
</div>
</section>
</div>
{{template "shell-end" .}}
{{end}}