Add last login tracking, batch date auto-fill, and bug fixes
- Track last_login_at on User model, updated on every successful login - Show last login date in admin panel user table - Fix admin/garden date display (datetime strings already contain T separator) - Fix My Garden Internal Server Error (MySQL does not support NULLS LAST syntax) - Fix Log Batch infinite loop when user has zero varieties - Auto-fill batch dates from today when creating a new batch, calculated from selected variety's week offsets (germination, greenhouse, garden) - Update README with new features and batch date auto-fill formula table Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,7 @@ const api = {
|
||||
// ===== State =====
|
||||
let state = {
|
||||
varieties: [],
|
||||
varietiesLoaded: false,
|
||||
batches: [],
|
||||
settings: {},
|
||||
};
|
||||
@@ -150,7 +151,7 @@ function toast(msg, isError = false) {
|
||||
// ===== Utility =====
|
||||
function fmt(dateStr) {
|
||||
if (!dateStr) return '—';
|
||||
const d = new Date(dateStr + 'T00:00:00');
|
||||
const d = new Date(dateStr.includes('T') ? dateStr : dateStr + 'T00:00:00');
|
||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
}
|
||||
|
||||
@@ -408,6 +409,7 @@ function batchCard(b, compact = false) {
|
||||
async function loadVarieties() {
|
||||
try {
|
||||
state.varieties = await api.get('/varieties/');
|
||||
state.varietiesLoaded = true;
|
||||
renderVarieties();
|
||||
} catch (e) {
|
||||
document.getElementById('varieties-container').innerHTML = `<div class="empty-state">Error: ${esc(e.message)}</div>`;
|
||||
@@ -752,6 +754,7 @@ async function deleteVariety(id) {
|
||||
|
||||
// ===== Batch Modals =====
|
||||
function batchFormHtml(b = {}) {
|
||||
const isNew = !b.id;
|
||||
const varOpts = state.varieties.map(v =>
|
||||
`<option value="${v.id}" ${b.variety_id===v.id?'selected':''}>${v.name}${v.variety_name?' ('+v.variety_name+')':''}</option>`
|
||||
).join('');
|
||||
@@ -760,7 +763,7 @@ function batchFormHtml(b = {}) {
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Plant Variety *</label>
|
||||
<select id="bf-variety" class="form-select">${varOpts}</select>
|
||||
<select id="bf-variety" class="form-select" ${isNew ? 'onchange="App.autofillBatchDates(this.value)"' : ''}>${varOpts}</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Batch Label</label>
|
||||
@@ -821,10 +824,43 @@ function collectBatchForm() {
|
||||
};
|
||||
}
|
||||
|
||||
function showAddBatchModal() {
|
||||
function autofillBatchDates(varietyId) {
|
||||
const v = state.varieties.find(x => x.id === parseInt(varietyId));
|
||||
if (!v) return;
|
||||
|
||||
function addDays(dateStr, days) {
|
||||
const d = new Date(dateStr + 'T00:00:00');
|
||||
d.setDate(d.getDate() + days);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
// Sow = today; other dates calculated as offsets from sow using variety's relative week gaps
|
||||
const sowDate = today;
|
||||
const germDate = v.days_to_germinate
|
||||
? addDays(sowDate, v.days_to_germinate)
|
||||
: null;
|
||||
const ghDate = v.weeks_to_start != null && v.weeks_to_greenhouse != null
|
||||
? addDays(sowDate, (v.weeks_to_start - v.weeks_to_greenhouse) * 7)
|
||||
: null;
|
||||
const gardenDate = v.weeks_to_start != null && v.weeks_to_garden != null
|
||||
? addDays(sowDate, (v.weeks_to_start + v.weeks_to_garden) * 7)
|
||||
: null;
|
||||
|
||||
document.getElementById('bf-sow').value = sowDate;
|
||||
if (germDate) document.getElementById('bf-germ').value = germDate;
|
||||
if (ghDate) document.getElementById('bf-gh').value = ghDate;
|
||||
if (gardenDate) document.getElementById('bf-garden').value = gardenDate;
|
||||
}
|
||||
|
||||
async function showAddBatchModal() {
|
||||
if (!state.varietiesLoaded) {
|
||||
state.varieties = await api.get('/varieties/');
|
||||
state.varietiesLoaded = true;
|
||||
}
|
||||
if (!state.varieties.length) {
|
||||
// Load varieties first if not loaded
|
||||
api.get('/varieties/').then(v => { state.varieties = v; showAddBatchModal(); });
|
||||
openModal('Log a Batch', '<p style="color:var(--text-muted)">You need to add at least one seed variety before logging a batch. Go to <strong>Seed Library</strong> to add varieties.</p><div class="btn-row" style="margin-top:1rem"><button class="btn btn-secondary" onclick="App.closeModal()">Close</button></div>');
|
||||
return;
|
||||
}
|
||||
openModal('Log a Batch', `
|
||||
@@ -834,6 +870,9 @@ function showAddBatchModal() {
|
||||
<button class="btn btn-secondary" onclick="App.closeModal()">Cancel</button>
|
||||
</div>
|
||||
`);
|
||||
// Auto-fill dates for the default selected variety
|
||||
const defaultVarietyId = document.getElementById('bf-variety')?.value;
|
||||
if (defaultVarietyId) autofillBatchDates(defaultVarietyId);
|
||||
}
|
||||
|
||||
async function submitAddBatch() {
|
||||
@@ -851,7 +890,7 @@ async function submitAddBatch() {
|
||||
|
||||
async function showEditBatchModal(id) {
|
||||
try {
|
||||
if (!state.varieties.length) state.varieties = await api.get('/varieties/');
|
||||
if (!state.varietiesLoaded) { state.varieties = await api.get('/varieties/'); state.varietiesLoaded = true; }
|
||||
const b = state.batches.find(x => x.id === id) || await api.get(`/batches/${id}`);
|
||||
openModal('Edit Batch', `
|
||||
${batchFormHtml(b)}
|
||||
@@ -909,6 +948,7 @@ async function loadAdmin() {
|
||||
<tr>
|
||||
<th>Email</th>
|
||||
<th>Joined</th>
|
||||
<th>Last Login</th>
|
||||
<th>Varieties</th>
|
||||
<th>Batches</th>
|
||||
<th>Status</th>
|
||||
@@ -920,6 +960,7 @@ async function loadAdmin() {
|
||||
<tr id="admin-row-${u.id}">
|
||||
<td><span class="admin-email">${esc(u.email)}</span>${u.is_admin ? ' <span class="badge-admin">admin</span>' : ''}</td>
|
||||
<td class="admin-date">${fmt(u.created_at)}</td>
|
||||
<td class="admin-date">${u.last_login_at ? fmt(u.last_login_at) : '<span class="text-muted">Never</span>'}</td>
|
||||
<td class="admin-num">${u.variety_count}</td>
|
||||
<td class="admin-num">${u.batch_count}</td>
|
||||
<td><span class="status-pill ${u.is_disabled ? 'disabled' : 'active'}">${u.is_disabled ? 'Disabled' : 'Active'}</span></td>
|
||||
@@ -1091,7 +1132,7 @@ async function init() {
|
||||
// ===== Public API =====
|
||||
window.App = {
|
||||
showAddVarietyModal, showEditVarietyModal, submitAddVariety, submitEditVariety, deleteVariety,
|
||||
showAddBatchModal, showEditBatchModal, submitAddBatch, submitEditBatch, deleteBatch,
|
||||
showAddBatchModal, showEditBatchModal, submitAddBatch, submitEditBatch, deleteBatch, autofillBatchDates,
|
||||
filterVarieties, filterBatches,
|
||||
saveSettings, toggleNtfyAuth, sendTestNotification, sendDailySummary,
|
||||
adminViewUser, adminResetPassword, adminSubmitReset, adminToggleDisable, adminDeleteUser, adminSwitchTab,
|
||||
|
||||
Reference in New Issue
Block a user