Improve UI

- Add metrics table  to sidebar after schedule generation
- Add scheduler status indicator to sidebar
- Refactor report() to consume `ResidentMetrics`
- Delete unused preview component
- Beautify css across wizard steps
This commit is contained in:
2026-03-14 19:44:29 +02:00
parent 3ecdc91802
commit 756c1cdc47
14 changed files with 363 additions and 269 deletions

View File

@@ -118,4 +118,9 @@
body {
@apply bg-background text-foreground;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
}

View File

@@ -2,38 +2,38 @@
import Basic from "./components/configurations/basic.svelte";
import Residents from "./components/configurations/residents.svelte";
import Advanced from "./components/configurations/advanced.svelte";
import Preview from "./components/schedule/preview.svelte";
import { rota, steps } from "./state.svelte.js";
import { EngineStatus, rota, steps } from "./state.svelte.js";
import Generate from "./components/schedule/generate.svelte";
</script>
<main
class="grid h-screen w-full grid-cols-5 overflow-hidden bg-zinc-200/50 font-sans tracking-tight antialiased"
class="bg-zinc-200/ grid h-screen w-full grid-cols-5 overflow-hidden font-sans tracking-tight antialiased"
>
<aside
class="col-span-1 flex flex-col border-r border-zinc-200 bg-zinc-50/50 font-sans antialiased"
>
<div class="flex justify-center border p-3 font-sans">
<h1 class="text-xl font-black tracking-tight uppercase">Rota Scheduler</h1>
<div class="flex justify-center p-2 font-sans">
<h1 class="text-xl font-black tracking-tighter text-slate-900 uppercase">
Rota <span class="text-emerald-600">Scheduler</span>
</h1>
</div>
<div class="h-px w-full bg-zinc-200"></div>
<nav class="relative flex-1 p-6">
<div class="absolute top-10 bottom-10 left-9.75 w-0.5 bg-zinc-200"></div>
<div class="relative flex h-full flex-col justify-between">
<nav class="relative flex flex-col p-4">
<div class="flex flex-col gap-2">
{#each steps as step}
<button
onclick={() => (rota.currentStep = step.id)}
class="group relative z-10 flex items-center gap-4 py-4 transition-all"
class="group relative z-10 flex items-center gap-4 rounded-xl p-4 transition-all
{rota.currentStep === step.id ? 'bg-zinc-200/80 shadow-inner' : 'hover:bg-zinc-100/50'}"
>
<div
class="flex size-8 items-center justify-center rounded-full border-2 transition-all duration-300
class="flex size-8 items-center justify-center rounded-lg transition-all duration-300
{rota.currentStep === step.id
? 'bg-slate-800 text-white'
? 'bg-slate-800 text-white shadow-md'
: rota.currentStep > step.id
? 'bg-emerald-600 text-white'
: 'bg-white text-zinc-400'}"
: 'border border-zinc-200 bg-white text-zinc-400'}"
>
{#if rota.currentStep > step.id}
<svg
@@ -54,7 +54,7 @@
<div class="flex flex-col items-start">
<span
class="text-[10px] font-bold tracking-widest uppercase
{rota.currentStep === step.id ? 'text-black-800' : 'text-zinc-400'}"
{rota.currentStep === step.id ? 'text-zinc-600' : 'text-zinc-400'}"
>
ΒΗΜΑ {step.id}
</span>
@@ -70,19 +70,130 @@
</div>
</nav>
<div class="border-t border-zinc-200 bg-white p-6">
<div class="rounded-xl border border-zinc-100 bg-zinc-50 p-4">
<div class="mb-2 flex items-center justify-between">
<span class="text-[10px] font-bold text-zinc-500 uppercase">ΟΛΟΚΛΗΡΩΣΗ</span>
<span class="text-[10px] font-bold text-zinc-500"
>{(((rota.currentStep - 1) / (steps.length - 1)) * 100).toFixed(0)}%</span
{#if rota.metrics.length > 0}
<div class="h-px w-full bg-zinc-200"></div>
<div class="flex flex-1 flex-col py-4">
<p class="px-6 pb-2 text-[10px] font-black tracking-widest text-zinc-400 uppercase">
Δικαιωσυνη
</p>
<div class="w-full overflow-hidden px-2 text-[10px]">
<table class="w-full">
<thead>
<tr class="border-b border-zinc-300 bg-zinc-50">
<th
class="border-r border-zinc-300 px-2 py-1 text-left font-bold text-zinc-500 uppercase last:border-r-0"
>Ειδικευομενος</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>ΣΥΝ</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>Α1</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>Α2</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>Κ</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>ΣΚ/Α</th
>
</tr>
</thead>
<tbody>
{#each rota.metrics as m}
<tr class="border-b border-zinc-200 last:border-0 hover:bg-zinc-50">
<td
class="border-r border-zinc-200 px-2 py-1 font-medium text-zinc-700 last:border-r-0"
>{m.name}</td
>
<td
class="border-r border-zinc-200 px-1 py-1 text-center font-bold text-zinc-800 last:border-r-0"
>{m.total}</td
>
<td
class="border-r border-zinc-200 px-1 py-1 text-center text-zinc-500 last:border-r-0"
>{m.open_first}</td
>
<td
class="border-r border-zinc-200 px-1 py-1 text-center text-zinc-500 last:border-r-0"
>{m.open_second}</td
>
<td
class="border-r border-zinc-200 px-1 py-1 text-center text-zinc-500 last:border-r-0"
>{m.closed}</td
>
<td
class="border-r border-zinc-200 px-1 py-1 text-center text-zinc-500 last:border-r-0"
>{m.holiday}</td
>
</tr>
{/each}
</tbody>
<tfoot>
<tr class="border-t border-zinc-300 bg-zinc-50">
<th
class="border-r border-zinc-300 px-2 py-1 text-left font-bold text-zinc-500 uppercase last:border-r-0"
>Συνολο</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>{rota.metrics.reduce((s, m) => s + m.total, 0)}</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>{rota.metrics.reduce((s, m) => s + m.open_first, 0)}</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>{rota.metrics.reduce((s, m) => s + m.open_second, 0)}</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>{rota.metrics.reduce((s, m) => s + m.closed, 0)}</th
>
<th
class="border-r border-zinc-300 px-1 py-1 text-center font-bold text-zinc-500 last:border-r-0"
>{rota.metrics.reduce((s, m) => s + m.holiday, 0)}</th
>
</tr>
</tfoot>
</table>
</div>
</div>
{/if}
<div class="h-px w-full bg-zinc-200"></div>
<div class="space-y-4 p-4">
<div
class="border-l-2 py-2 pl-3 transition-colors
{rota.engineStatus === EngineStatus.Running
? 'border-amber-400'
: rota.engineStatus === EngineStatus.Success
? 'border-emerald-500'
: rota.engineStatus === EngineStatus.Error
? 'border-red-500'
: 'border-zinc-300'}"
>
<div class="mb-2 flex items-center gap-2">
<span class="text-[10px] font-black tracking-widest text-zinc-400 uppercase"
>ΚΑΤΑΣΤΑΣΗ ΜΗΧΑΝΗΣ</span
>
</div>
<div class="h-1.5 w-full overflow-hidden rounded-full bg-zinc-200">
<div
class="h-full bg-emerald-600 transition-all duration-500"
style="width: {(((rota.currentStep - 1) / (steps.length - 1)) * 100).toFixed(0)}%"
></div>
<div class="flex flex-col gap-1">
<span class="text-[11px] font-bold text-zinc-800 uppercase">
{rota.engineStatus}
</span>
<p class="text-[10px] leading-tight font-medium text-zinc-500">
{rota.lastMessage}
</p>
</div>
</div>
</div>
@@ -97,8 +208,6 @@
{:else if rota.currentStep === 3}
<Advanced />
{:else if rota.currentStep === 4}
<Preview />
{:else if rota.currentStep === 5}
<Generate />
{/if}
</div>

View File

@@ -29,73 +29,57 @@
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
</header>
{#if rota.forbiddenPairs.length > 0}
<div class="space-y-3">
{#each rota.forbiddenPairs as pair, i (pair)}
<div
class="group flex items-center gap-4 rounded-2xl border border-zinc-200 bg-white p-4 transition-all hover:border-blue-200 hover:bg-white"
<div class="space-y-2">
{#each rota.forbiddenPairs as pair, i (pair)}
<div class="flex items-center gap-2">
<select
bind:value={pair.id1}
class="flex-1 appearance-none rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 transition-all outline-none focus:border-zinc-400 focus:ring-4 focus:ring-zinc-100"
>
<div class="flex flex-1 flex-col gap-1">
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">
Resident A
</p>
<select
bind:value={pair.id1}
class="w-full rounded-xl border border-zinc-100 bg-zinc-50 px-3 py-2 text-sm font-semibold text-zinc-700 outline-none focus:ring-2"
>
{#each rota.residents as r}
<option value={r.id}>{r.name || "Unnamed Resident"}</option>
{/each}
</select>
</div>
{#each rota.residents as r}
<option value={r.id}>{r.name || "Ανώνυμος"}</option>
{/each}
</select>
<div class="flex flex-1 flex-col gap-1">
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">
Resident B
</p>
<select
bind:value={pair.id2}
class="w-full rounded-xl border border-zinc-100 bg-zinc-50 px-3 py-2 text-sm font-semibold text-zinc-700 outline-none focus:ring-2"
>
{#each rota.residents as r}
{#if r.id !== pair.id1}
<option value={r.id}>{r.name || "Unnamed Resident"}</option>
{/if}
{/each}
</select>
</div>
<span class="text-sm font-bold text-zinc-400">×</span>
<button
onclick={() => rota.removeForbiddenPair(i)}
class="mt-5 rounded-lg p-2 text-zinc-300 transition-colors"
aria-label="Remove forbidden pair"
<select
bind:value={pair.id2}
class="flex-1 appearance-none rounded-lg border border-zinc-200 bg-white px-3 py-1.5 text-sm font-medium text-zinc-700 transition-all outline-none focus:border-zinc-400 focus:ring-4 focus:ring-zinc-100"
>
{#each rota.residents as r}
{#if r.id !== pair.id1}
<option value={r.id}>{r.name || "Ανώνυμος"}</option>
{/if}
{/each}
</select>
<button
onclick={() => rota.removeForbiddenPair(i)}
class="rounded-lg p-1.5 text-zinc-400 transition-colors hover:text-red-500 active:scale-90"
aria-label="Remove forbidden pair"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
><path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path
d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"
/><line x1="10" x2="10" y1="11" y2="17" /><line
x1="14"
x2="14"
y1="11"
y2="17"
/></svg
>
</button>
</div>
{/each}
</div>
{/if}
<path d="M3 6h18" /><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" /><path
d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"
/><line x1="10" x2="10" y1="11" y2="17" /><line x1="14" x2="14" y1="11" y2="17" />
</svg>
</button>
</div>
{/each}
</div>
<div class="mt-8 flex justify-center">
<div class="mt-6 flex justify-center">
<Button
onclick={() => rota.addForbiddenPair()}
variant="outline"

View File

@@ -40,7 +40,7 @@
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">MONTH</p>
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">ΜΗΝΑΣ</p>
<div class="relative">
<select
bind:value={rota.selectedMonth}
@@ -68,7 +68,7 @@
</div>
<div class="space-y-2">
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">YEAR</p>
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">ΧΡΟΝΟΣ</p>
<div class="relative">
<select
bind:value={rota.selectedYear}
@@ -97,7 +97,7 @@
</div>
<div class="mt-8 space-y-2">
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">HOLIDAYS</p>
<p class="ml-1 text-[10px] font-black tracking-widest text-zinc-500 uppercase">ΑΡΓΙΕΣ</p>
<Popover.Root>
<Popover.Trigger>

View File

@@ -33,17 +33,17 @@
</header>
{#each rota.residents as resident, residentIndex (resident.id)}
<div
class="group relative flex flex-col gap-6 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 transition-all hover:border-blue-200 hover:bg-white"
class="group relative flex flex-col gap-6 rounded-2xl border border-zinc-200 bg-zinc-50 p-6 transition-all hover:border-zinc-300 hover:bg-white"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">
Resident {residentIndex + 1}
ΕΙΔΙΚΕΥΟΜΕΝΟΣ {residentIndex + 1}
</span>
</div>
<button
onclick={() => rota.removeResident(resident.id)}
class="text-zinc-500 transition-colors"
class="text-zinc-400 transition-colors hover:text-red-500 active:scale-90 active:text-red-700"
aria-label="Remove resident"
>
<svg
@@ -65,23 +65,23 @@
<div class="grid grid-cols-12 items-end gap-6">
<div class="col-span-4 space-y-2">
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">Full Name</p>
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">ΟΝΟΜΑ</p>
<input
bind:value={resident.name}
placeholder="π.χ. Τάκης Τσουκαλάς"
class="w-full rounded-xl border border-zinc-200 bg-white px-4 py-2.5 text-sm outline-none hover:border-blue-200 hover:bg-white"
class="w-full rounded-xl border border-zinc-200 bg-white px-4 py-2.5 text-sm outline-none hover:border-zinc-300 hover:bg-white"
/>
</div>
<div class="col-span-4 space-y-2">
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">Negative Shifts</p>
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">ΑΡΝΗΤΙΚΕΣ ΕΦΗΜΕΡΙΕΣ</p>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-full justify-start rounded-xl border-zinc-200 bg-white px-4 py-5 font-normal hover:border-blue-200 hover:bg-white"
class="w-full justify-start rounded-xl border-zinc-200 bg-white px-4 py-5 font-normal hover:border-zinc-300 hover:bg-white"
>
<span class="mr-2 text-zinc-500"
><svg
@@ -190,14 +190,14 @@
</div>
<div class="col-span-4 space-y-2">
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">Manual Shifts</p>
<p class="ml-1 text-xs font-bold text-zinc-500 uppercase">ΑΝΑΓΚΑΣΤΙΚΕΣ ΕΦΗΜΕΡΙΕΣ</p>
<Popover.Root>
<Popover.Trigger>
{#snippet child({ props })}
<Button
{...props}
variant="outline"
class="w-full justify-start rounded-xl border-zinc-200 bg-white px-4 py-5 font-normal hover:border-blue-200 hover:bg-white"
class="w-full justify-start rounded-xl border-zinc-200 bg-white px-4 py-5 font-normal hover:border-zinc-300 hover:bg-white"
>
<span class="mr-2 text-zinc-500"
><svg
@@ -307,22 +307,23 @@
<div
class="grid grid-cols-3 divide-x divide-zinc-200 rounded-xl border border-zinc-200 bg-white py-3"
>
<div class="flex flex-col items-center space-y-2 px-4">
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">Max Shifts</p>
<div class="flex items-center gap-2">
<input
type="number"
bind:value={resident.maxShifts}
placeholder="-"
class="w-16 rounded-lg border border-zinc-100 bg-zinc-50 py-1 text-center text-sm font-bold text-zinc-700 outline-none placeholder:text-zinc-300"
/>
</div>
<div class="flex flex-col items-center gap-2 px-4">
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">
ΜΕΓΙΣΤΟΣ ΦΟΡΤΟΣ
</p>
<input
type="number"
bind:value={resident.maxShifts}
class="w-8 rounded border border-zinc-200 bg-white py-1 text-center text-sm font-bold text-zinc-700 transition-all outline-none hover:border-zinc-300 focus:border-zinc-400 focus:ring-4 focus:ring-zinc-100"
/>
</div>
<div class="flex flex-col items-center space-y-2 px-4">
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">Shift Types</p>
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">
ΤΥΠΟΙ ΕΦΗΜΕΡΙΩΝ
</p>
<div class="flex gap-1">
{#each ["Closed", "OpenFirst", "OpenSecond"] as type}
{#each [["Closed", "Κλειστή"], ["OpenFirst", "Ανοιχτή 1"], ["OpenSecond", "Ανοιχτή 2"]] as [type, label]}
{@const active = resident.allowedTypes.includes(type)}
<button
type="button"
@@ -338,29 +339,27 @@
? 'border-zinc-800 bg-zinc-800 text-white'
: 'border-zinc-200 bg-white text-zinc-500 hover:bg-zinc-50 hover:text-zinc-600'}"
>
{type.replace("OpenAs", "")}
{label}
</button>
{/each}
</div>
</div>
<div class="flex flex-col items-center space-y-2 px-4">
<div class="flex flex-col items-center gap-2 px-4">
<p class="text-[10px] font-black tracking-widest text-zinc-500 uppercase">
Reduced Workload
ΜΕΙΩΣΗ ΦΟΡΤΟΥ
</p>
<button
role="switch"
aria-checked={resident.reducedLoad}
onclick={() => (resident.reducedLoad = !resident.reducedLoad)}
class="flex items-center gap-2 rounded-lg border px-3 py-1 transition-all hover:border-blue-200 hover:bg-white
{resident.reducedLoad
? 'border-green-200 bg-green-50 text-green-700'
: 'border-zinc-200 bg-white text-zinc-500'}"
class="relative h-5 w-9 shrink-0 rounded-full transition-colors duration-200
{resident.reducedLoad ? 'bg-green-500' : 'bg-zinc-200'}"
>
<div
class="size-2 rounded-full {resident.reducedLoad
? 'animate-pulse bg-green-500'
: 'bg-zinc-300'}"
class="absolute top-0.5 left-0.5 size-4 rounded-full bg-white shadow transition-transform duration-200
{resident.reducedLoad ? 'translate-x-4' : 'translate-x-0'}"
></div>
<span class="text-[10px] font-bold uppercase">-1</span>
</button>
</div>
</div>
@@ -371,7 +370,8 @@
<Button
onclick={() => rota.addResident()}
variant="outline"
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
disabled={rota.residents.some((r) => !r.name.trim())}
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95 disabled:cursor-not-allowed disabled:opacity-40"
>
Προσθήκη Ειδικευόμενου
</Button>

View File

@@ -1,7 +1,14 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import { invoke } from "@tauri-apps/api/core";
import { type MonthlyScheduleDTO, rota, steps, type ShiftPosition } from "../../state.svelte.js";
import {
EngineStatus,
type MonthlyScheduleDTO,
type ResidentMetrics,
rota,
steps,
type ShiftPosition
} from "../../state.svelte.js";
function getResidentName(day: number, pos: ShiftPosition) {
const residentId = rota.solution[`${day}-${pos}`];
@@ -41,23 +48,24 @@
async function generate() {
let config = rota.toDTO();
console.log(config);
rota.engineStatus = EngineStatus.Running;
rota.lastMessage = "";
try {
let schedule = await invoke<MonthlyScheduleDTO>("generate", { config });
console.log(schedule);
rota.solution = schedule;
rota.solution = await invoke<MonthlyScheduleDTO>("generate", { config });
rota.metrics = await invoke<ResidentMetrics[]>("get_metrics");
rota.engineStatus = EngineStatus.Success;
} catch (error) {
const { kind, details } = error as AppError;
rota.engineStatus = EngineStatus.Error;
rota.lastMessage = details;
console.error(`[${kind}] - ${details}`);
}
}
async function export_file() {
let schedule = rota.solution;
try {
await invoke("export", { schedule });
await invoke("export");
} catch (error) {
const { kind, details } = error as AppError;
console.error(`[${kind}] - ${details}`);
@@ -91,10 +99,10 @@
{/each}
</div>
<div class="grid auto-rows-fr grid-cols-7 gap-px bg-zinc-200">
{#each rota.emptySlots as _}<div class="bg-zinc-50/30"></div>{/each}
{#each rota.emptySlots as _}<div class="min-h-25 bg-zinc-100/60"></div>{/each}
{#each rota.daysArray as day (day)}
{@const slotCount = getRequiredSlots(day)}
<div class="group min-h-25 bg-white p-2 transition-all hover:bg-blue-50/30">
<div class="group min-h-25 bg-white p-2 transition-all hover:bg-teal-50/30">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-black text-zinc-500">{day}</span>
</div>
@@ -103,7 +111,7 @@
{#if slotCount > 0}
<button
onclick={() => "handleCellClick(day, 1)"}
class="w-full overflow-hidden rounded border border-pink-200 bg-pink-50 px-1.5 py-1 text-left text-[10px] font-bold text-pink-600 transition-colors hover:bg-pink-100"
class="w-full overflow-hidden rounded border border-rose-200 bg-rose-50 px-1.5 py-1 text-left text-[10px] font-bold text-rose-700 transition-colors hover:bg-rose-100"
>
{getResidentName(day, "First")}
</button>
@@ -111,7 +119,7 @@
{#if slotCount > 1}
<button
onclick={() => "handleCellClick(day, 2)"}
class="w-full overflow-hidden rounded border border-emerald-200 bg-emerald-50 px-1.5 py-1 text-left text-[10px] font-bold text-emerald-600 transition-colors hover:bg-emerald-100"
class="w-full overflow-hidden rounded border border-emerald-200 bg-emerald-50 px-1.5 py-1 text-left text-[10px] font-bold text-emerald-700 transition-colors hover:bg-emerald-100"
>
{getResidentName(day, "Second")}
</button>
@@ -119,6 +127,9 @@
</div>
</div>
{/each}
{#each Array.from( { length: (7 - ((rota.emptySlots.length + rota.daysArray.length) % 7)) % 7 } ) as _}
<div class="min-h-25 bg-zinc-100/60"></div>
{/each}
</div>
</div>

View File

@@ -1,88 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button/index.js";
import { rota, steps } from "../../state.svelte.js";
function getResidentName(day: number, slot: number) {
const assignedResidents = rota.residents.filter((resident) =>
resident.manualShifts.some(
(shift) =>
shift.day === day &&
shift.month === rota.selectedMonth &&
shift.year === rota.selectedYear
)
);
const resident = assignedResidents[slot - 1];
return resident ? resident.name : "-";
}
// 2 slots in odd days, 1 slot in even days
function getRequiredSlots(day: number) {
return day % 2 === 0 ? 1 : 2;
}
</script>
<div class="mb-6 flex items-center justify-between">
<h2 class="text-2xl font-bold text-zinc-800">{steps[rota.currentStep - 1].title}</h2>
<div class="justify-end">
<Button
onclick={() => (rota.currentStep -= 1)}
variant="outline"
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
>
Προηγούμενο
</Button>
<Button
onclick={() => (rota.currentStep += 1)}
variant="outline"
class="border-zinc-200 bg-white text-zinc-600 shadow-sm transition-all hover:bg-zinc-50 hover:text-zinc-900 active:scale-95"
>
Επόμενο
</Button>
</div>
</div>
<header class="mb-2">
<p class="text-sm text-zinc-500">{steps[rota.currentStep - 1].description}</p>
</header>
<div class="overflow-hidden rounded-xl border border-zinc-200 bg-white">
<div class="grid grid-cols-7 border-b border-zinc-200 bg-zinc-50/50">
{#each ["ΔΕΥΤΕΡΑ", "ΤΡΙΤΗ", "ΤΕΤΑΡΤΗ", "ΠΕΜΠΤΗ", "ΠΑΡΑΣΚΕΥΗ", "ΣΑΒΒΑΤΟ", "ΚΥΡΙΑΚΗ"] as dayName}
<div class="py-2 text-center text-[10px] font-bold tracking-widest text-zinc-500 uppercase">
{dayName}
</div>
{/each}
</div>
<div class="grid auto-rows-fr grid-cols-7 gap-px bg-zinc-200">
{#each rota.emptySlots as _}<div class="bg-zinc-50/30"></div>{/each}
{#each rota.daysArray as day (day)}
{@const slotCount = getRequiredSlots(day)}
<div class="group min-h-25 bg-white p-2 transition-all hover:bg-blue-50/30">
<div class="mb-2 flex items-center justify-between">
<span class="text-xs font-black text-zinc-500">{day}</span>
</div>
<div class="space-y-1">
{#if slotCount > 0}
<button
onclick={() => "handleCellClick(day, 1)"}
class="w-full overflow-hidden rounded border border-pink-200 bg-pink-50 px-1.5 py-1 text-left text-[10px] font-bold text-pink-600 transition-colors hover:bg-pink-100"
>
{getResidentName(day, 1)}
</button>
{/if}
{#if slotCount == 2}
<button
onclick={() => "handleCellClick(day, 2)"}
class="w-full overflow-hidden rounded border border-emerald-200 bg-emerald-50 px-1.5 py-1 text-left text-[10px] font-bold text-emerald-600 transition-colors hover:bg-emerald-100"
>
{getResidentName(day, 2)}
</button>
{/if}
</div>
</div>
{/each}
</div>
</div>

View File

@@ -16,6 +16,15 @@ export interface ForbiddenPair {
id2: number;
}
export type ResidentMetrics = {
name: string;
total: number;
open_first: number;
open_second: number;
closed: number;
holiday: number;
};
export class RotaState {
currentStep = $state(1);
residentsCounter = $state(0);
@@ -25,6 +34,11 @@ export class RotaState {
holidays = $state<CalendarDate[]>([]);
forbiddenPairs = $state<ForbiddenPair[]>([]);
engineStatus = $state(EngineStatus.Idle);
lastMessage = $state("");
metrics: ResidentMetrics[] = $state([]);
projectMonth = $state(new CalendarDate(2026, 2, 1));
syncProjectMonth() {
@@ -50,7 +64,9 @@ export class RotaState {
}
removeResident(id: number) {
this.residents = this.residents.filter((r) => r.id !== id);
const index = this.residents.findIndex((r) => r.id === id);
if (index !== -1) this.residents.splice(index, 1);
this.forbiddenPairs = this.forbiddenPairs.filter((p) => p.id1 !== id && p.id2 !== id);
}
findResident(id: number) {
@@ -92,6 +108,13 @@ export class RotaState {
}
}
export enum EngineStatus {
Idle = "ΣΕ ΑΝΑΜΟΝΗ",
Running = "Ο ΑΛΓΟΡΙΘΜΟΣ ΤΡΕΧΕΙ...",
Success = "ΕΠΙΤΥΧΗΣ ΔΗΜΙΟΥΡΓΙΑ",
Error = "ΣΦΑΛΜΑ ΣΥΣΤΗΜΑΤΟΣ"
}
export const rota = new RotaState();
export type MonthlyScheduleDTO = {
@@ -141,11 +164,6 @@ export const steps = [
},
{
id: 4,
title: "Επισκόπηση",
description: "Έλεγξε το πρόγραμμα με τις υποχρεωτικές υπάρχουσες εφημερίες."
},
{
id: 5,
title: "Δημιουργία",
description: "Τρέξε τον αλγόριθμο ανάθεσης εφημεριών, εξήγαγε τα αποτελέσματα."
}