Months shown setting and style colors

This commit is contained in:
marijo 2026-06-09 07:29:50 +00:00
parent 0727798af7
commit 2ea0d9363e
5 changed files with 55 additions and 46 deletions

View file

@ -19,7 +19,8 @@ The SQLite database is created automatically in `data/go.sqlite`.
- Feriefridage must be used before `31 August` and expire before September planning. - Feriefridage must be used before `31 August` and expire before September planning.
- FLEX days use `7.5h` Monday-Thursday and `7h` Friday. - FLEX days use `7.5h` Monday-Thursday and `7h` Friday.
- Start balances are configured separately from current FLEX hours. - Start balances are configured separately from current FLEX hours.
- Monthly vacation-day corrections can be entered, e.g. `+1.2` or `-2.6` days. - The planner shows configurable numbers of previous and future months around the current month.
- Monthly vacation-day corrections can be entered directly in the planner table, e.g. `+1.2` or `-2.6` days.
- Each date-range entry can individually allow unpaid fallback so only that range is converted to unpaid days if balances are insufficient. - Each date-range entry can individually allow unpaid fallback so only that range is converted to unpaid days if balances are insufficient.
## Data ## Data

View file

@ -122,6 +122,8 @@ function migrate(PDO $db): void
'planning_start_month' => '1', 'planning_start_month' => '1',
'planning_years' => '3', 'planning_years' => '3',
'planning_months' => '36', 'planning_months' => '36',
'previous_months' => '5',
'future_months' => '30',
'vacation_days_per_month' => '2.08', 'vacation_days_per_month' => '2.08',
'feriefridage_per_year' => '5', 'feriefridage_per_year' => '5',
'feriefridage_expiry_month' => '8', 'feriefridage_expiry_month' => '8',
@ -156,6 +158,8 @@ function save_settings(PDO $db, array $settings): void
'planning_start_month', 'planning_start_month',
'planning_years', 'planning_years',
'planning_months', 'planning_months',
'previous_months',
'future_months',
'vacation_days_per_month', 'vacation_days_per_month',
'feriefridage_per_year', 'feriefridage_per_year',
'feriefridage_expiry_month', 'feriefridage_expiry_month',

23
app.js
View file

@ -6,7 +6,7 @@ const state = {
}; };
const settingKeys = [ const settingKeys = [
'planning_start_year', 'planning_start_month', 'planning_months', 'previous_months', 'future_months',
'vacation_days_per_month', 'feriefridage_per_year', 'feriefridage_expiry_month', 'vacation_days_per_month', 'feriefridage_per_year', 'feriefridage_expiry_month',
'normal_day_hours', 'friday_hours', 'opening_vacation_days', 'normal_day_hours', 'friday_hours', 'opening_vacation_days',
'opening_feriefridage_days', 'opening_flex_hours' 'opening_feriefridage_days', 'opening_flex_hours'
@ -55,8 +55,8 @@ async function loadState() {
function normalizeSettings(settings) { function normalizeSettings(settings) {
const normalized = { ...settings }; const normalized = { ...settings };
normalized.planning_start_month = Number(normalized.planning_start_month || 1); normalized.previous_months = Number(normalized.previous_months ?? 5);
normalized.planning_months = Number(normalized.planning_months || (Number(normalized.planning_years || 3) * 12)); normalized.future_months = Number(normalized.future_months ?? 30);
return normalized; return normalized;
} }
@ -338,9 +338,13 @@ function renderMonthRow(month) {
} }
function calculatePlan(settings, entries, corrections = {}) { function calculatePlan(settings, entries, corrections = {}) {
const startYear = Number(settings.planning_start_year || new Date().getFullYear()); const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate());
const startMonth = Number(settings.planning_start_month || 1); const previousMonths = Number(settings.previous_months ?? 5);
const monthCount = Number(settings.planning_months || (Number(settings.planning_years || 3) * 12)); const futureMonths = Number(settings.future_months ?? 30);
const startCursor = dateFromParts(today.getFullYear(), today.getMonth() + 1 - previousMonths, 1);
const startYear = startCursor.getFullYear();
const startMonth = startCursor.getMonth() + 1;
const monthCount = previousMonths + futureMonths + 1;
const startDate = dateFromParts(startYear, startMonth, 1); const startDate = dateFromParts(startYear, startMonth, 1);
const endDate = dateFromParts(startYear, startMonth + monthCount, 0); const endDate = dateFromParts(startYear, startMonth + monthCount, 0);
const months = []; const months = [];
@ -371,7 +375,6 @@ function calculatePlan(settings, entries, corrections = {}) {
} }
const avgDayHours = averageDayHours(settings); const avgDayHours = averageDayHours(settings);
const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate());
const currentFlexHours = Number(settings.current_flex_hours ?? flexBalance); const currentFlexHours = Number(settings.current_flex_hours ?? flexBalance);
for (let i = 0; i < monthCount; i++) { for (let i = 0; i < monthCount; i++) {
@ -504,8 +507,10 @@ function calculatePlan(settings, entries, corrections = {}) {
function calculateCurrentSnapshot(settings, entries, corrections = {}) { function calculateCurrentSnapshot(settings, entries, corrections = {}) {
const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()); const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate());
const startYear = Number(settings.planning_start_year || today.getFullYear()); const previousMonths = Number(settings.previous_months ?? 5);
const startMonth = Number(settings.planning_start_month || 1); const startCursor = dateFromParts(today.getFullYear(), today.getMonth() + 1 - previousMonths, 1);
const startYear = startCursor.getFullYear();
const startMonth = startCursor.getMonth() + 1;
const startDate = dateFromParts(startYear, startMonth, 1); const startDate = dateFromParts(startYear, startMonth, 1);
const avgDayHours = averageDayHours(settings); const avgDayHours = averageDayHours(settings);
const warnings = []; const warnings = [];

View file

@ -114,9 +114,8 @@
<button type="button" id="closeSettingsDialogBtn" class="ghost small">Close</button> <button type="button" id="closeSettingsDialogBtn" class="ghost small">Close</button>
</div> </div>
<form id="settingsForm" class="form-grid compact"> <form id="settingsForm" class="form-grid compact">
<label>Start year<input id="planning_start_year" type="number" step="1"></label> <label>Previous months shown<input id="previous_months" type="number" step="1" min="0" max="120"></label>
<label>Start month<input id="planning_start_month" type="number" step="1" min="1" max="12"></label> <label>Future months shown<input id="future_months" type="number" step="1" min="0" max="120"></label>
<label>Months shown<input id="planning_months" type="number" step="1" min="1" max="120"></label>
<label>Vacation/month<input id="vacation_days_per_month" type="number" step="0.01"></label> <label>Vacation/month<input id="vacation_days_per_month" type="number" step="0.01"></label>
<label>Feriefridage/year<input id="feriefridage_per_year" type="number" step="0.01"></label> <label>Feriefridage/year<input id="feriefridage_per_year" type="number" step="0.01"></label>
<label>FF deadline month<input id="feriefridage_expiry_month" type="number" step="1" min="1" max="12"></label> <label>FF deadline month<input id="feriefridage_expiry_month" type="number" step="1" min="1" max="12"></label>

View file

@ -1,15 +1,15 @@
:root { :root {
--bg: #111315; --bg: #18120F;
--panel: #1B1E22; --panel: #241A16;
--ink: #F3F4F6; --ink: #F5EADC;
--muted: #A0A4AB; --muted: #B9A89A;
--brand: #C46A3A; --brand: #C9874A;
--brand-dark: #d97b48; --brand-dark: #E0A15F;
--accent: #3BA7A0; --accent: #A38A63;
--good: #3BA7A0; --good: #A38A63;
--warn: #C46A3A; --warn: #C9874A;
--bad: #B32626; --bad: #D96B5F;
--line: rgba(59,167,160,.35); --line: rgba(163,138,99,.32);
--shadow: 0 2px 16px rgba(0, 0, 0, 0.55); --shadow: 0 2px 16px rgba(0, 0, 0, 0.55);
--radius: 14px; --radius: 14px;
} }
@ -19,7 +19,7 @@
body { body {
margin: 0; margin: 0;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: radial-gradient(circle at top left, rgba(59,167,160,.16), transparent 32rem), radial-gradient(circle at top right, rgba(196,106,58,.14), transparent 30rem), var(--bg); background: radial-gradient(circle at top left, rgba(163,138,99,.16), transparent 32rem), radial-gradient(circle at top right, rgba(201,135,74,.14), transparent 30rem), var(--bg);
color: var(--ink); color: var(--ink);
} }
@ -28,8 +28,8 @@ body {
top: 0; top: 0;
z-index: 50; z-index: 50;
padding: .35rem clamp(.45rem, 2vw, 1.25rem); padding: .35rem clamp(.45rem, 2vw, 1.25rem);
background: linear-gradient(180deg, rgba(27,30,34,.94), rgba(27,30,34,.78)); background: linear-gradient(180deg, rgba(36,26,22,.96), rgba(36,26,22,.82));
border-bottom: 1px solid rgba(59,167,160,.35); border-bottom: 1px solid rgba(163,138,99,.32);
box-shadow: 0 2px 16px #0008; box-shadow: 0 2px 16px #0008;
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
} }
@ -74,9 +74,9 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
.header-entries { .header-entries {
margin: .25rem 0 .25rem; margin: .25rem 0 .25rem;
padding: .25rem .35rem; padding: .25rem .35rem;
border: 1px solid rgba(59,167,160,.28); border: 1px solid rgba(163,138,99,.26);
border-radius: 10px; border-radius: 10px;
background: rgba(17,19,21,.72); background: rgba(24,18,15,.72);
} }
.header-entries .table-wrap { overflow-x: auto; overflow-y: visible; } .header-entries .table-wrap { overflow-x: auto; overflow-y: visible; }
.compact-entries-table { font-size: .74rem; } .compact-entries-table { font-size: .74rem; }
@ -92,14 +92,14 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
min-height: 1.45rem; min-height: 1.45rem;
padding: .22rem .45rem; padding: .22rem .45rem;
border-radius: 999px; border-radius: 999px;
background: rgba(196,106,58,.18); background: rgba(201,135,74,.18);
color: var(--brand-dark); color: var(--brand-dark);
border: 1px solid rgba(196,106,58,.55); border: 1px solid rgba(201,135,74,.50);
font-size: .72rem; font-size: .72rem;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
} }
.header-warning.bad { background: rgba(139,30,30,.35); color: #ffb4b4; border-color: rgba(179,38,38,.7); } .header-warning.bad { background: rgba(126,59,50,.36); color: #F1B5A9; border-color: rgba(217,107,95,.55); }
.header-action-button { .header-action-button {
appearance: none; appearance: none;
display: inline-flex; display: inline-flex;
@ -108,7 +108,7 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
min-height: 1.45rem; min-height: 1.45rem;
padding: .22rem .45rem; padding: .22rem .45rem;
border-radius: 999px; border-radius: 999px;
background: #111315; background: #18120F;
color: var(--ink); color: var(--ink);
border: 1px solid var(--accent); border: 1px solid var(--accent);
font-size: .72rem; font-size: .72rem;
@ -116,10 +116,10 @@ h2 { margin-bottom: 0; font-size: 1.1rem; }
cursor: pointer; cursor: pointer;
line-height: 1; line-height: 1;
} }
.header-action-button:hover { background: var(--accent); color: #111315; border-color: var(--accent); transform: none; } .header-action-button:hover { background: var(--accent); color: #18120F; border-color: var(--accent); transform: none; }
.planner-column-header { .planner-column-header {
margin-top: .25rem; margin-top: .25rem;
border-top: 1px solid rgba(59,167,160,.25); border-top: 1px solid rgba(163,138,99,.24);
padding-top: .2rem; padding-top: .2rem;
} }
.planner-header-table th { border-bottom: 0; } .planner-header-table th { border-bottom: 0; }
@ -144,8 +144,8 @@ main {
} }
.card, .panel { .card, .panel {
background: rgba(27,30,34,.92); background: rgba(36,26,22,.92);
border: 1px solid rgba(59,167,160,.35); border: 1px solid rgba(163,138,99,.32);
border-radius: var(--radius); border-radius: var(--radius);
box-shadow: var(--shadow); box-shadow: var(--shadow);
} }
@ -192,10 +192,10 @@ input, select, textarea {
padding: .7rem .8rem; padding: .7rem .8rem;
font: inherit; font: inherit;
color: var(--ink); color: var(--ink);
background: #111315; background: #18120F;
} }
textarea { resize: vertical; } textarea { resize: vertical; }
input:focus, select:focus, textarea:focus { outline: 3px solid rgba(196,106,58,.22); border-color: var(--brand); } input:focus, select:focus, textarea:focus { outline: 3px solid rgba(201,135,74,.22); border-color: var(--brand); }
.checkbox-row { .checkbox-row {
display: flex; display: flex;
@ -220,13 +220,13 @@ button {
} }
button:hover { background: var(--brand-dark); transform: translateY(-1px); } button:hover { background: var(--brand-dark); transform: translateY(-1px); }
button.ghost { button.ghost {
background: #111315; background: #18120F;
color: var(--ink); color: var(--ink);
border: 1px solid var(--accent); border: 1px solid var(--accent);
} }
button.ghost:hover { background: var(--accent); color: #111315; } button.ghost:hover { background: var(--accent); color: #18120F; }
button.small { padding: .36rem .55rem; font-size: .78rem; } button.small { padding: .36rem .55rem; font-size: .78rem; }
button.danger { background: #8B1E1E; color: var(--ink); border: 1px solid #8B1E1E; } button.danger { background: #7E3B32; color: var(--ink); border: 1px solid #7E3B32; }
.actions { display: flex; justify-content: flex-end; gap: .6rem; } .actions { display: flex; justify-content: flex-end; gap: .6rem; }
.help-text { color: var(--muted); margin: 0; font-size: .9rem; } .help-text { color: var(--muted); margin: 0; font-size: .9rem; }
.hidden { display: none !important; } .hidden { display: none !important; }
@ -291,8 +291,8 @@ tbody tr:hover { background: rgba(255,255,255,.04); }
font-weight: 800; font-weight: 800;
} }
.pill.approved, .pill.confirmed { background: #1f3b32; color: var(--ink); } .pill.approved, .pill.confirmed { background: #1f3b32; color: var(--ink); }
.pill.unpaid { background: rgba(196,106,58,.22); color: var(--brand-dark); } .pill.unpaid { background: rgba(201,135,74,.22); color: var(--brand-dark); }
.important-col { background: rgba(59,167,160,.12); font-size: 1rem; } .important-col { background: rgba(163,138,99,.13); font-size: 1rem; }
.correction-cell { min-width: 5.2rem; } .correction-cell { min-width: 5.2rem; }
.correction-input { .correction-input {
width: 4.8rem; width: 4.8rem;
@ -304,11 +304,11 @@ tbody tr:hover { background: rgba(255,255,255,.04); }
.warnings { display: grid; gap: .55rem; } .warnings { display: grid; gap: .55rem; }
.warning { .warning {
border-left: 4px solid var(--warn); border-left: 4px solid var(--warn);
background: rgba(196,106,58,.16); background: rgba(201,135,74,.16);
padding: .65rem .75rem; padding: .65rem .75rem;
border-radius: 12px; border-radius: 12px;
} }
.warning.bad { border-left-color: var(--bad); background: rgba(139,30,30,.32); } .warning.bad { border-left-color: var(--bad); background: rgba(126,59,50,.34); }
.warning.good { border-left-color: var(--good); background: rgba(31,59,50,.75); } .warning.good { border-left-color: var(--good); background: rgba(31,59,50,.75); }
.years { display: grid; gap: 1.25rem; } .years { display: grid; gap: 1.25rem; }