diff --git a/README.md b/README.md index 2617847..754771e 100644 --- a/README.md +++ b/README.md @@ -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. - FLEX days use `7.5h` Monday-Thursday and `7h` Friday. - 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. ## Data diff --git a/api.php b/api.php index 493b000..62be7de 100644 --- a/api.php +++ b/api.php @@ -122,6 +122,8 @@ function migrate(PDO $db): void 'planning_start_month' => '1', 'planning_years' => '3', 'planning_months' => '36', + 'previous_months' => '5', + 'future_months' => '30', 'vacation_days_per_month' => '2.08', 'feriefridage_per_year' => '5', 'feriefridage_expiry_month' => '8', @@ -156,6 +158,8 @@ function save_settings(PDO $db, array $settings): void 'planning_start_month', 'planning_years', 'planning_months', + 'previous_months', + 'future_months', 'vacation_days_per_month', 'feriefridage_per_year', 'feriefridage_expiry_month', diff --git a/app.js b/app.js index 3cdc86c..db40751 100644 --- a/app.js +++ b/app.js @@ -6,7 +6,7 @@ const state = { }; const settingKeys = [ - 'planning_start_year', 'planning_start_month', 'planning_months', + 'previous_months', 'future_months', 'vacation_days_per_month', 'feriefridage_per_year', 'feriefridage_expiry_month', 'normal_day_hours', 'friday_hours', 'opening_vacation_days', 'opening_feriefridage_days', 'opening_flex_hours' @@ -55,8 +55,8 @@ async function loadState() { function normalizeSettings(settings) { const normalized = { ...settings }; - normalized.planning_start_month = Number(normalized.planning_start_month || 1); - normalized.planning_months = Number(normalized.planning_months || (Number(normalized.planning_years || 3) * 12)); + normalized.previous_months = Number(normalized.previous_months ?? 5); + normalized.future_months = Number(normalized.future_months ?? 30); return normalized; } @@ -338,9 +338,13 @@ function renderMonthRow(month) { } function calculatePlan(settings, entries, corrections = {}) { - const startYear = Number(settings.planning_start_year || new Date().getFullYear()); - const startMonth = Number(settings.planning_start_month || 1); - const monthCount = Number(settings.planning_months || (Number(settings.planning_years || 3) * 12)); + const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()); + const previousMonths = Number(settings.previous_months ?? 5); + 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 endDate = dateFromParts(startYear, startMonth + monthCount, 0); const months = []; @@ -371,7 +375,6 @@ function calculatePlan(settings, entries, corrections = {}) { } 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); for (let i = 0; i < monthCount; i++) { @@ -504,8 +507,10 @@ function calculatePlan(settings, entries, corrections = {}) { function calculateCurrentSnapshot(settings, entries, corrections = {}) { const today = dateFromParts(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()); - const startYear = Number(settings.planning_start_year || today.getFullYear()); - const startMonth = Number(settings.planning_start_month || 1); + const previousMonths = Number(settings.previous_months ?? 5); + 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 avgDayHours = averageDayHours(settings); const warnings = []; diff --git a/index.html b/index.html index 61e451e..c509b5f 100644 --- a/index.html +++ b/index.html @@ -114,9 +114,8 @@
- - - + + diff --git a/style.css b/style.css index 77af4fb..3ad772e 100644 --- a/style.css +++ b/style.css @@ -1,15 +1,15 @@ :root { - --bg: #111315; - --panel: #1B1E22; - --ink: #F3F4F6; - --muted: #A0A4AB; - --brand: #C46A3A; - --brand-dark: #d97b48; - --accent: #3BA7A0; - --good: #3BA7A0; - --warn: #C46A3A; - --bad: #B32626; - --line: rgba(59,167,160,.35); + --bg: #18120F; + --panel: #241A16; + --ink: #F5EADC; + --muted: #B9A89A; + --brand: #C9874A; + --brand-dark: #E0A15F; + --accent: #A38A63; + --good: #A38A63; + --warn: #C9874A; + --bad: #D96B5F; + --line: rgba(163,138,99,.32); --shadow: 0 2px 16px rgba(0, 0, 0, 0.55); --radius: 14px; } @@ -19,7 +19,7 @@ body { margin: 0; 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); } @@ -28,8 +28,8 @@ body { top: 0; z-index: 50; padding: .35rem clamp(.45rem, 2vw, 1.25rem); - background: linear-gradient(180deg, rgba(27,30,34,.94), rgba(27,30,34,.78)); - border-bottom: 1px solid rgba(59,167,160,.35); + background: linear-gradient(180deg, rgba(36,26,22,.96), rgba(36,26,22,.82)); + border-bottom: 1px solid rgba(163,138,99,.32); box-shadow: 0 2px 16px #0008; backdrop-filter: blur(12px); } @@ -74,9 +74,9 @@ h2 { margin-bottom: 0; font-size: 1.1rem; } .header-entries { margin: .25rem 0 .25rem; padding: .25rem .35rem; - border: 1px solid rgba(59,167,160,.28); + border: 1px solid rgba(163,138,99,.26); 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; } .compact-entries-table { font-size: .74rem; } @@ -92,14 +92,14 @@ h2 { margin-bottom: 0; font-size: 1.1rem; } min-height: 1.45rem; padding: .22rem .45rem; border-radius: 999px; - background: rgba(196,106,58,.18); + background: rgba(201,135,74,.18); color: var(--brand-dark); - border: 1px solid rgba(196,106,58,.55); + border: 1px solid rgba(201,135,74,.50); font-size: .72rem; font-weight: 800; 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 { appearance: none; display: inline-flex; @@ -108,7 +108,7 @@ h2 { margin-bottom: 0; font-size: 1.1rem; } min-height: 1.45rem; padding: .22rem .45rem; border-radius: 999px; - background: #111315; + background: #18120F; color: var(--ink); border: 1px solid var(--accent); font-size: .72rem; @@ -116,10 +116,10 @@ h2 { margin-bottom: 0; font-size: 1.1rem; } cursor: pointer; 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 { margin-top: .25rem; - border-top: 1px solid rgba(59,167,160,.25); + border-top: 1px solid rgba(163,138,99,.24); padding-top: .2rem; } .planner-header-table th { border-bottom: 0; } @@ -144,8 +144,8 @@ main { } .card, .panel { - background: rgba(27,30,34,.92); - border: 1px solid rgba(59,167,160,.35); + background: rgba(36,26,22,.92); + border: 1px solid rgba(163,138,99,.32); border-radius: var(--radius); box-shadow: var(--shadow); } @@ -192,10 +192,10 @@ input, select, textarea { padding: .7rem .8rem; font: inherit; color: var(--ink); - background: #111315; + background: #18120F; } 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 { display: flex; @@ -220,13 +220,13 @@ button { } button:hover { background: var(--brand-dark); transform: translateY(-1px); } button.ghost { - background: #111315; + background: #18120F; color: var(--ink); 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.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; } .help-text { color: var(--muted); margin: 0; font-size: .9rem; } .hidden { display: none !important; } @@ -291,8 +291,8 @@ tbody tr:hover { background: rgba(255,255,255,.04); } font-weight: 800; } .pill.approved, .pill.confirmed { background: #1f3b32; color: var(--ink); } -.pill.unpaid { background: rgba(196,106,58,.22); color: var(--brand-dark); } -.important-col { background: rgba(59,167,160,.12); font-size: 1rem; } +.pill.unpaid { background: rgba(201,135,74,.22); color: var(--brand-dark); } +.important-col { background: rgba(163,138,99,.13); font-size: 1rem; } .correction-cell { min-width: 5.2rem; } .correction-input { width: 4.8rem; @@ -304,11 +304,11 @@ tbody tr:hover { background: rgba(255,255,255,.04); } .warnings { display: grid; gap: .55rem; } .warning { border-left: 4px solid var(--warn); - background: rgba(196,106,58,.16); + background: rgba(201,135,74,.16); padding: .65rem .75rem; 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); } .years { display: grid; gap: 1.25rem; }