Compare commits
10 Commits
d46c4c3fe7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
dbd8b6e6ac
|
|||
|
1a85ca6830
|
|||
|
bc3e178676
|
|||
|
b2f84a27cd
|
|||
|
36f6d5135f
|
|||
|
06b70f814e
|
|||
|
369c8b872e
|
|||
|
2146fa6bad
|
|||
|
3defdc3aab
|
|||
|
08f92c068e
|
433
app.py
433
app.py
@@ -1,225 +1,231 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
"""
|
||||
Minimal Time Clock Application
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
LEGACY_TIMEZONE = timezone.utc
|
||||
|
||||
from zoneinfo import ZoneInfo
|
||||
from flask import (
|
||||
Flask,
|
||||
request,
|
||||
render_template,
|
||||
jsonify
|
||||
)
|
||||
import logging
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Flask Configuration
|
||||
# -----------------------------------------------------------------------------
|
||||
LEGACY_TIMEZONE = ZoneInfo("America/New_York")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
DATABASE = "database/timeclock.db"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
def is_timezone_aware(dt):
|
||||
return dt.tzinfo is not None and dt.tzinfo.utcoffset(dt) is not None
|
||||
|
||||
def make_timezone_aware(dt, tzstr):
|
||||
if tzstr:
|
||||
tzval = datetime.strptime(tzstr.replace(":", ""), "%z").tzinfo
|
||||
else:
|
||||
tzval = LEGACY_TIMEZONE
|
||||
if not is_timezone_aware(dt):
|
||||
return dt.replace(tzinfo=tzval)
|
||||
else:
|
||||
return dt
|
||||
|
||||
def get_db_connection():
|
||||
"""
|
||||
Create and return a sqlite3 database connection.
|
||||
"""
|
||||
|
||||
conn = sqlite3.connect(DATABASE)
|
||||
conn.row_factory = sqlite3.Row
|
||||
|
||||
return conn
|
||||
|
||||
|
||||
def initialize_database():
|
||||
"""
|
||||
Create database tables if they do not already exist.
|
||||
"""
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
|
||||
cursor.execute("""
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
entrytype TEXT NOT NULL CHECK(entrytype IN ('in', 'out')),
|
||||
ts TEXT NOT NULL,
|
||||
paid BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
comments TEXT,
|
||||
FOREIGN KEY(user_id) REFERENCES users(id)
|
||||
)
|
||||
""")
|
||||
|
||||
conn.commit()
|
||||
|
||||
cursor.execute("SELECT COUNT(*) AS count FROM users")
|
||||
|
||||
result = cursor.fetchone()
|
||||
|
||||
if result["count"] == 0:
|
||||
cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
|
||||
cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
|
||||
cursor.execute("INSERT INTO users (name) VALUES ('Charlie')")
|
||||
|
||||
conn.commit()
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Time Helpers
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def parse_iso_datetime(value):
|
||||
"""
|
||||
Parse timestamps into timezone-aware datetime objects.
|
||||
|
||||
Handles both:
|
||||
2026-05-29 08:00:00
|
||||
and:
|
||||
2026-05-29T08:00:00-04:00
|
||||
"""
|
||||
|
||||
#
|
||||
# Support legacy sqlite format:
|
||||
# 2026-05-29 08:00:00
|
||||
#
|
||||
|
||||
normalized = value.replace(" ", "T")
|
||||
|
||||
dt = datetime.fromisoformat(normalized)
|
||||
|
||||
#
|
||||
# If timestamp is naive, assume legacy timezone.
|
||||
#
|
||||
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=LEGACY_TIMEZONE)
|
||||
|
||||
return dt
|
||||
|
||||
def utc_now():
|
||||
"""
|
||||
Return current UTC timestamp.
|
||||
"""
|
||||
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Utility Functions
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
def create_entry(user_id, entry_type, client_timestamp=None):
|
||||
"""
|
||||
Create a clock in/out entry for the specified user.
|
||||
"""
|
||||
def est_now():
|
||||
return datetime.now(ZoneInfo("America/New_York"))
|
||||
|
||||
def pay_hours(user_id, begin_date, end_date):
|
||||
conn = get_db_connection()
|
||||
|
||||
conn.set_trace_callback(logging.error)
|
||||
cursor = conn.cursor()
|
||||
|
||||
if client_timestamp:
|
||||
timestamp = parse_iso_datetime(client_timestamp)
|
||||
else:
|
||||
timestamp = utc_now()
|
||||
|
||||
cursor.execute("""
|
||||
INSERT INTO entries (user_id, entrytype, ts)
|
||||
VALUES (?, ?, ?)
|
||||
""", (
|
||||
user_id,
|
||||
entry_type,
|
||||
timestamp.isoformat()
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
def generate_report(user_id, begin_date, end_date):
|
||||
"""
|
||||
Generate total worked hours for a user between two dates.
|
||||
"""
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
begin_dt = parse_iso_datetime(begin_date)
|
||||
end_dt = parse_iso_datetime(end_date)
|
||||
|
||||
cursor.execute("""
|
||||
SELECT entrytype, ts
|
||||
FROM entries
|
||||
UPDATE entries
|
||||
SET paid = TRUE
|
||||
WHERE user_id = ?
|
||||
ORDER BY ts ASC
|
||||
""", (user_id,))
|
||||
|
||||
AND datetime(ts) BETWEEN datetime(?) AND datetime(?)
|
||||
""", (user_id, begin_dt.isoformat(), end_dt.isoformat()))
|
||||
rows = cursor.fetchall()
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def create_entry(user_id, entry_type, client_timestamp=None, comments=None, client_timezone=None):
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
if client_timestamp:
|
||||
timestamp = make_timezone_aware(parse_iso_datetime(client_timestamp), client_timezone)
|
||||
else:
|
||||
timestamp = est_now()
|
||||
if comments:
|
||||
cursor.execute("""
|
||||
INSERT INTO entries (user_id, entrytype, ts, paid, comments)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (user_id, entry_type, timestamp.isoformat(), False, comments))
|
||||
else:
|
||||
cursor.execute("""
|
||||
INSERT INTO entries (user_id, entrytype, ts, paid)
|
||||
VALUES (?, ?, ?, ?)
|
||||
""", (user_id, entry_type, timestamp.isoformat(), False))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def generate_report(user_id, begin_date, end_date, client_timezone):
|
||||
logging.error(f"user_id={user_id}, begin_date={begin_date}, end_date={end_date}")
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
begin_dt = make_timezone_aware(parse_iso_datetime(begin_date), client_timezone)
|
||||
end_dt = make_timezone_aware(parse_iso_datetime(end_date), client_timezone)
|
||||
logging.error(f"begin_dt={begin_dt}, end_dt={end_dt}")
|
||||
cursor.execute("""
|
||||
SELECT entrytype, ts, paid, comments
|
||||
FROM entries
|
||||
WHERE user_id = ?
|
||||
AND datetime(ts) BETWEEN datetime(?) AND datetime(?)
|
||||
ORDER BY ts ASC
|
||||
""", (user_id, begin_dt.isoformat(), end_dt.isoformat()))
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
total_seconds = 0
|
||||
|
||||
clock_in_time = None
|
||||
|
||||
paid_seconds = 0
|
||||
actions = []
|
||||
for row in rows:
|
||||
|
||||
timestamp = parse_iso_datetime(row["ts"])
|
||||
|
||||
if timestamp < begin_dt or timestamp > end_dt:
|
||||
continue
|
||||
|
||||
if row["entrytype"] == "in":
|
||||
clock_in_time = timestamp
|
||||
|
||||
elif row["entrytype"] == "out" and clock_in_time is not None:
|
||||
|
||||
delta = timestamp - clock_in_time
|
||||
|
||||
if row["paid"]:
|
||||
paid_seconds += delta.total_seconds()
|
||||
total_seconds += delta.total_seconds()
|
||||
|
||||
clock_in_time = None
|
||||
|
||||
if row["comments"]:
|
||||
if row["paid"]:
|
||||
actions.append("(PAID) {}".format(row["comments"]))
|
||||
else:
|
||||
actions.append(row["comments"])
|
||||
total_hours = total_seconds / 3600.0
|
||||
|
||||
return round(total_hours, 2)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Web UI Routes
|
||||
# -----------------------------------------------------------------------------
|
||||
paid_hours = paid_seconds / 3600.0
|
||||
return round(total_hours, 2), round(paid_hours, 2), actions
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
"""
|
||||
Main application page.
|
||||
"""
|
||||
|
||||
report_hours = None
|
||||
user_report = {}
|
||||
all_user_reports = []
|
||||
selected_user_id = None
|
||||
now = est_now()
|
||||
default_begin = now - timedelta(days=7)
|
||||
default_end = now
|
||||
conn = get_db_connection()
|
||||
|
||||
users = conn.execute("""
|
||||
SELECT id, name
|
||||
FROM users
|
||||
ORDER BY name
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
if request.method == "POST":
|
||||
selected_user_id = request.form.get("user_id")
|
||||
action = request.form.get("action")
|
||||
client_timestamp = request.form.get("client_timestamp")
|
||||
client_timezone = request.form.get("client_timezone")
|
||||
comments = request.form.get("comments")
|
||||
if action == "clock_in":
|
||||
create_entry(
|
||||
selected_user_id,
|
||||
"in",
|
||||
client_timestamp,
|
||||
comments,
|
||||
client_timezone
|
||||
)
|
||||
elif action == "clock_out":
|
||||
create_entry(
|
||||
selected_user_id,
|
||||
"out",
|
||||
client_timestamp,
|
||||
comments,
|
||||
client_timezone
|
||||
)
|
||||
elif action == "pay":
|
||||
begin_date = request.form.get("begin_date")
|
||||
end_date = request.form.get("end_date")
|
||||
pay_hours(
|
||||
selected_user_id,
|
||||
begin_date,
|
||||
end_date,
|
||||
client_timezone
|
||||
)
|
||||
elif action == "report":
|
||||
begin_date = request.form.get("begin_date")
|
||||
end_date = request.form.get("end_date")
|
||||
client_timezone = request.form.get("client_timezone")
|
||||
report_hours, paid_hours, actions = generate_report(
|
||||
selected_user_id,
|
||||
begin_date,
|
||||
end_date,
|
||||
client_timezone
|
||||
)
|
||||
user_report = {
|
||||
"total_hours": report_hours,
|
||||
"paid_hours": paid_hours,
|
||||
"actions": actions
|
||||
}
|
||||
default_begin = parse_iso_datetime(begin_date)
|
||||
default_end = parse_iso_datetime(end_date)
|
||||
for user in users:
|
||||
total_hours, paid_hours, actions = generate_report(
|
||||
user["id"],
|
||||
default_begin.isoformat(),
|
||||
default_end.isoformat(),
|
||||
request.form.get("client_timezone")
|
||||
)
|
||||
all_user_reports.append({
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"total_hours": total_hours,
|
||||
"paid_hours": paid_hours,
|
||||
"actions": actions
|
||||
})
|
||||
conn = get_db_connection()
|
||||
clocked_in_users = conn.execute("""
|
||||
SELECT u.id, u.name, last_entry.ts
|
||||
FROM users u
|
||||
@@ -238,98 +244,19 @@ def index():
|
||||
WHERE last_entry.entrytype = 'in'
|
||||
ORDER BY u.name
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
clocked_in_list = []
|
||||
|
||||
for user in clocked_in_users:
|
||||
|
||||
ts = parse_iso_datetime(user["ts"])
|
||||
|
||||
clocked_in_list.append({
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"since": ts.isoformat()
|
||||
})
|
||||
|
||||
report_hours = None
|
||||
|
||||
all_user_reports = []
|
||||
|
||||
selected_user_id = None
|
||||
|
||||
now = utc_now()
|
||||
|
||||
default_begin = now - timedelta(days=7)
|
||||
|
||||
default_end = now
|
||||
|
||||
if request.method == "POST":
|
||||
|
||||
selected_user_id = request.form.get("user_id")
|
||||
|
||||
action = request.form.get("action")
|
||||
|
||||
client_timestamp = request.form.get("client_timestamp")
|
||||
|
||||
if action == "clock_in":
|
||||
|
||||
create_entry(
|
||||
selected_user_id,
|
||||
"in",
|
||||
client_timestamp
|
||||
)
|
||||
|
||||
elif action == "clock_out":
|
||||
|
||||
create_entry(
|
||||
selected_user_id,
|
||||
"out",
|
||||
client_timestamp
|
||||
)
|
||||
|
||||
elif action == "report":
|
||||
|
||||
begin_date = request.form.get("begin_date")
|
||||
|
||||
end_date = request.form.get("end_date")
|
||||
|
||||
report_hours = generate_report(
|
||||
selected_user_id,
|
||||
begin_date,
|
||||
end_date
|
||||
)
|
||||
|
||||
default_begin = parse_iso_datetime(begin_date)
|
||||
|
||||
default_end = parse_iso_datetime(end_date)
|
||||
|
||||
else:
|
||||
|
||||
#
|
||||
# Automatically generate last 7 day report
|
||||
# for ALL users
|
||||
#
|
||||
|
||||
for user in users:
|
||||
|
||||
hours = generate_report(
|
||||
user["id"],
|
||||
default_begin.isoformat(),
|
||||
default_end.isoformat()
|
||||
)
|
||||
|
||||
all_user_reports.append({
|
||||
"id": user["id"],
|
||||
"name": user["name"],
|
||||
"hours": hours
|
||||
})
|
||||
|
||||
return render_template(
|
||||
"index.html",
|
||||
users=users,
|
||||
report_hours=report_hours,
|
||||
user_report=user_report,
|
||||
all_user_reports=all_user_reports,
|
||||
clocked_in_users=clocked_in_list,
|
||||
default_begin=default_begin.isoformat(timespec="minutes"),
|
||||
@@ -337,76 +264,8 @@ def index():
|
||||
selected_user_id=selected_user_id
|
||||
)
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# CRUD API
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@app.route("/api/users", methods=["GET"])
|
||||
def api_get_users():
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
users = conn.execute("""
|
||||
SELECT id, name
|
||||
FROM users
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify([dict(user) for user in users])
|
||||
|
||||
|
||||
@app.route("/api/entries", methods=["GET"])
|
||||
def api_get_entries():
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
entries = conn.execute("""
|
||||
SELECT id, user_id, entrytype, ts
|
||||
FROM entries
|
||||
ORDER BY ts DESC
|
||||
""").fetchall()
|
||||
|
||||
conn.close()
|
||||
|
||||
return jsonify([dict(entry) for entry in entries])
|
||||
|
||||
|
||||
@app.route("/api/entries", methods=["POST"])
|
||||
def api_create_entry():
|
||||
|
||||
data = request.get_json()
|
||||
|
||||
user_id = data.get("user_id")
|
||||
|
||||
entry_type = data.get("entrytype")
|
||||
|
||||
client_timestamp = data.get("timestamp")
|
||||
|
||||
if entry_type not in ("in", "out"):
|
||||
|
||||
return jsonify({
|
||||
"error": "Invalid entrytype"
|
||||
}), 400
|
||||
|
||||
create_entry(
|
||||
user_id,
|
||||
entry_type,
|
||||
client_timestamp
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 201
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Application Entry Point
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
initialize_database()
|
||||
|
||||
app.run(
|
||||
host="0.0.0.0",
|
||||
port=5000,
|
||||
|
||||
@@ -4,7 +4,7 @@ replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: registry.home.aklabs.net/timeclock
|
||||
tag: 2026052901
|
||||
tag: 2026061501
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
|
||||
@@ -38,12 +38,22 @@
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Total Hours Worked</th>
|
||||
<th>Paid Hours</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
|
||||
{% for report in all_user_reports %}
|
||||
<tr>
|
||||
<td>{{ report.name }}</td>
|
||||
<td>{{ report.hours }}</td>
|
||||
<td>{{ report.total_hours }}</td>
|
||||
<td>{{ report.paid_hours }}</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for action in report.actions %}
|
||||
<li>{{ action }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
@@ -58,6 +68,12 @@
|
||||
|
||||
<form method="POST" id="timeclock-form">
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
id="client_timezone"
|
||||
name="client_timezone"
|
||||
value=""/>
|
||||
|
||||
<input
|
||||
type="hidden"
|
||||
name="client_timestamp"
|
||||
@@ -82,7 +98,6 @@
|
||||
{{ user.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
|
||||
</select>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -110,6 +125,15 @@
|
||||
Clock Out
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
name="action"
|
||||
value="pay"
|
||||
onclick="setCurrentTimestamp()"
|
||||
>
|
||||
Pay
|
||||
</button>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -121,8 +145,7 @@
|
||||
type="datetime-local"
|
||||
name="begin_date"
|
||||
id="begin_date"
|
||||
value=""
|
||||
>
|
||||
value="">
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -139,6 +162,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Comments</td>
|
||||
|
||||
<td>
|
||||
<textarea
|
||||
name="comments"
|
||||
id="comments"
|
||||
rows="5" cols="72"
|
||||
></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>Report</td>
|
||||
|
||||
@@ -159,7 +194,7 @@
|
||||
|
||||
<br>
|
||||
|
||||
{% if report_hours is not none %}
|
||||
{% if user_report is not none %}
|
||||
|
||||
<h2>Custom Report</h2>
|
||||
|
||||
@@ -167,10 +202,20 @@
|
||||
|
||||
<tr>
|
||||
<th>Total Hours Worked</th>
|
||||
<th>Paid Hours</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td>{{ report_hours }}</td>
|
||||
<td>{{ user_report.total_hours }}</td>
|
||||
<td>{{ user_report.paid_hours }}</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for action in user_report.actions %}
|
||||
<li>{{ action }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</table>
|
||||
@@ -179,10 +224,21 @@
|
||||
|
||||
<script>
|
||||
|
||||
function getFormattedOffset() {
|
||||
const offset = new Date().getTimezoneOffset();
|
||||
const absMinutes = Math.abs(offset);
|
||||
|
||||
// Invert the sign to match standard UTC notation
|
||||
const sign = offset <= 0 ? "+" : "-";
|
||||
|
||||
const hours = String(Math.floor(absMinutes / 60)).padStart(2, "0");
|
||||
const minutes = String(absMinutes % 60).padStart(2, "0");
|
||||
|
||||
return `${sign}${hours}:${minutes}`;
|
||||
}
|
||||
|
||||
function localDatetimeValue(date) {
|
||||
|
||||
const pad = (v) => String(v).padStart(2, "0");
|
||||
|
||||
return (
|
||||
date.getFullYear() + "-" +
|
||||
pad(date.getMonth() + 1) + "-" +
|
||||
@@ -193,43 +249,22 @@ function localDatetimeValue(date) {
|
||||
}
|
||||
|
||||
function setCurrentTimestamp() {
|
||||
|
||||
document.getElementById("client_timestamp").value =
|
||||
new Date().toISOString();
|
||||
document.getElementById("client_timestamp").value = new Date().toISOString();
|
||||
}
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
|
||||
//
|
||||
// Populate default report range
|
||||
//
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const weekAgo = new Date();
|
||||
|
||||
weekAgo.setDate(now.getDate() - 7);
|
||||
|
||||
document.getElementById("begin_date").value =
|
||||
localDatetimeValue(weekAgo);
|
||||
|
||||
document.getElementById("end_date").value =
|
||||
localDatetimeValue(now);
|
||||
|
||||
//
|
||||
// Convert displayed timestamps to local timezone
|
||||
//
|
||||
|
||||
document.getElementById("client_timezone").value = getFormattedOffset();
|
||||
document.getElementById("begin_date").value = localDatetimeValue(weekAgo);
|
||||
document.getElementById("end_date").value = localDatetimeValue(now);
|
||||
document.querySelectorAll(".tztime").forEach((el) => {
|
||||
|
||||
const ts = el.dataset.ts;
|
||||
|
||||
const d = new Date(ts);
|
||||
|
||||
el.textContent = d.toLocaleString();
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user