diff --git a/local_backend/main.py b/local_backend/main.py
index 8beef6d..99c26ec 100644
--- a/local_backend/main.py
+++ b/local_backend/main.py
@@ -20,6 +20,7 @@ import models.settings # noqa: F401
import models.flag # noqa: F401 — registers TableFlagDef, TableFlagAssignment
import models.message # noqa: F401 — registers StaffMessage, StaffMessageAck, QuickMessageTemplate
import models.reservation # noqa: F401
+import models.notes # noqa: F401 — registers SiteNote, SiteTodo
from routers import auth, tables, products, orders, waiters, reports, system, setup as setup_router
from routers import business_day as business_day_router
@@ -31,6 +32,7 @@ from routers import sse as sse_router
from routers import data_transfer as data_transfer_router
from routers import connect_orders as connect_orders_router
from routers import reservations as reservations_router
+from routers import notes as notes_router
def _run_migrations():
@@ -269,6 +271,25 @@ def _run_migrations():
# Phase 2B — staff payroll
"ALTER TABLE users ADD COLUMN hourly_rate REAL",
"ALTER TABLE waiter_shifts ADD COLUMN hourly_rate_snapshot REAL",
+ # Phase 2C — notes & todos
+ """CREATE TABLE IF NOT EXISTS site_notes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ body TEXT NOT NULL,
+ created_by_id INTEGER NOT NULL REFERENCES users(id),
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ is_pinned INTEGER NOT NULL DEFAULT 0
+ )""",
+ """CREATE TABLE IF NOT EXISTS site_todos (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ body TEXT NOT NULL,
+ is_done INTEGER NOT NULL DEFAULT 0,
+ done_at DATETIME,
+ done_by_id INTEGER REFERENCES users(id),
+ created_by_id INTEGER NOT NULL REFERENCES users(id),
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ priority VARCHAR NOT NULL DEFAULT 'normal'
+ )""",
]
for sql in migrations:
try:
@@ -331,3 +352,4 @@ app.include_router(sse_router.router, prefix="/api/sse", tag
app.include_router(data_transfer_router.router, prefix="/api/data-transfer", tags=["data-transfer"])
app.include_router(connect_orders_router.router, prefix="/api/connect", tags=["connect"])
app.include_router(reservations_router.router, prefix="/api/reservations", tags=["reservations"])
+app.include_router(notes_router.router, prefix="/api/notes", tags=["notes"])
diff --git a/local_backend/models/notes.py b/local_backend/models/notes.py
new file mode 100644
index 0000000..b3d1e45
--- /dev/null
+++ b/local_backend/models/notes.py
@@ -0,0 +1,37 @@
+from sqlalchemy import Column, Integer, String, Boolean, DateTime, Text, ForeignKey
+from sqlalchemy.orm import relationship
+from datetime import datetime, timezone
+from database import Base
+
+
+def _utcnow():
+ return datetime.now(timezone.utc)
+
+
+class SiteNote(Base):
+ __tablename__ = "site_notes"
+
+ id = Column(Integer, primary_key=True, index=True)
+ body = Column(Text, nullable=False)
+ created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+ created_at = Column(DateTime(timezone=True), default=_utcnow)
+ updated_at = Column(DateTime(timezone=True), default=_utcnow, onupdate=_utcnow)
+ is_pinned = Column(Boolean, default=False, nullable=False)
+
+ created_by = relationship("User", foreign_keys=[created_by_id])
+
+
+class SiteTodo(Base):
+ __tablename__ = "site_todos"
+
+ id = Column(Integer, primary_key=True, index=True)
+ body = Column(Text, nullable=False)
+ is_done = Column(Boolean, default=False, nullable=False)
+ done_at = Column(DateTime(timezone=True), nullable=True)
+ done_by_id = Column(Integer, ForeignKey("users.id"), nullable=True)
+ created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False)
+ created_at = Column(DateTime(timezone=True), default=_utcnow)
+ priority = Column(String, default="normal", nullable=False) # "normal" | "high"
+
+ created_by = relationship("User", foreign_keys=[created_by_id])
+ done_by = relationship("User", foreign_keys=[done_by_id])
diff --git a/local_backend/routers/notes.py b/local_backend/routers/notes.py
new file mode 100644
index 0000000..c198160
--- /dev/null
+++ b/local_backend/routers/notes.py
@@ -0,0 +1,151 @@
+from datetime import datetime, timezone
+from fastapi import APIRouter, Depends, HTTPException, status
+from sqlalchemy.orm import Session
+from typing import List
+
+from database import get_db
+from models.notes import SiteNote, SiteTodo
+from models.user import User
+from schemas.notes import NoteCreate, NoteUpdate, NoteOut, TodoCreate, TodoUpdate, TodoOut
+from routers.deps import require_manager
+
+router = APIRouter()
+
+
+def _utcnow():
+ return datetime.now(timezone.utc)
+
+
+def _note_out(n: SiteNote) -> dict:
+ name = None
+ if n.created_by:
+ name = n.created_by.full_name or n.created_by.username
+ return {
+ "id": n.id,
+ "body": n.body,
+ "is_pinned": n.is_pinned,
+ "created_by_id": n.created_by_id,
+ "created_by_name": name,
+ "created_at": n.created_at,
+ "updated_at": n.updated_at,
+ }
+
+
+def _todo_out(t: SiteTodo) -> dict:
+ creator_name = None
+ if t.created_by:
+ creator_name = t.created_by.full_name or t.created_by.username
+ done_name = None
+ if t.done_by:
+ done_name = t.done_by.full_name or t.done_by.username
+ return {
+ "id": t.id,
+ "body": t.body,
+ "is_done": t.is_done,
+ "done_at": t.done_at,
+ "done_by_id": t.done_by_id,
+ "done_by_name": done_name,
+ "created_by_id": t.created_by_id,
+ "created_by_name": creator_name,
+ "created_at": t.created_at,
+ "priority": t.priority,
+ }
+
+
+# ── Notes ─────────────────────────────────────────────────────────────────────
+
+@router.get("/", response_model=List[NoteOut])
+def list_notes(db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ notes = db.query(SiteNote).order_by(
+ SiteNote.is_pinned.desc(), SiteNote.updated_at.desc()
+ ).all()
+ return [_note_out(n) for n in notes]
+
+
+@router.post("/", response_model=NoteOut, status_code=status.HTTP_201_CREATED)
+def create_note(body: NoteCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ note = SiteNote(body=body.body, is_pinned=body.is_pinned, created_by_id=user.id)
+ db.add(note)
+ db.commit()
+ db.refresh(note)
+ return _note_out(note)
+
+
+@router.put("/{note_id}", response_model=NoteOut)
+def update_note(note_id: int, body: NoteUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ note = db.query(SiteNote).filter(SiteNote.id == note_id).first()
+ if not note:
+ raise HTTPException(status_code=404, detail="Note not found")
+ if body.body is not None:
+ note.body = body.body
+ if body.is_pinned is not None:
+ note.is_pinned = body.is_pinned
+ note.updated_at = _utcnow()
+ db.commit()
+ db.refresh(note)
+ return _note_out(note)
+
+
+@router.delete("/{note_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_note(note_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ note = db.query(SiteNote).filter(SiteNote.id == note_id).first()
+ if not note:
+ raise HTTPException(status_code=404, detail="Note not found")
+ db.delete(note)
+ db.commit()
+
+
+# ── Todos ─────────────────────────────────────────────────────────────────────
+
+@router.get("/todos", response_model=List[TodoOut])
+def list_todos(done: bool = None, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ q = db.query(SiteTodo)
+ if done is not None:
+ q = q.filter(SiteTodo.is_done == done)
+ # undone high-priority first, then undone normal, then done (by created_at desc)
+ todos = q.order_by(
+ SiteTodo.is_done.asc(),
+ SiteTodo.priority.desc(),
+ SiteTodo.created_at.desc(),
+ ).all()
+ return [_todo_out(t) for t in todos]
+
+
+@router.post("/todos", response_model=TodoOut, status_code=status.HTTP_201_CREATED)
+def create_todo(body: TodoCreate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ todo = SiteTodo(body=body.body, priority=body.priority, created_by_id=user.id)
+ db.add(todo)
+ db.commit()
+ db.refresh(todo)
+ return _todo_out(todo)
+
+
+@router.put("/todos/{todo_id}", response_model=TodoOut)
+def update_todo(todo_id: int, body: TodoUpdate, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ todo = db.query(SiteTodo).filter(SiteTodo.id == todo_id).first()
+ if not todo:
+ raise HTTPException(status_code=404, detail="Todo not found")
+ if body.body is not None:
+ todo.body = body.body
+ if body.priority is not None:
+ todo.priority = body.priority
+ if body.is_done is not None:
+ todo.is_done = body.is_done
+ if body.is_done and not todo.done_at:
+ todo.done_at = _utcnow()
+ todo.done_by_id = user.id
+ elif not body.is_done:
+ todo.done_at = None
+ todo.done_by_id = None
+ db.commit()
+ db.refresh(todo)
+ return _todo_out(todo)
+
+
+@router.delete("/todos/{todo_id}", status_code=status.HTTP_204_NO_CONTENT)
+def delete_todo(todo_id: int, db: Session = Depends(get_db), user: User = Depends(require_manager)):
+ todo = db.query(SiteTodo).filter(SiteTodo.id == todo_id).first()
+ if not todo:
+ raise HTTPException(status_code=404, detail="Todo not found")
+ db.delete(todo)
+ db.commit()
diff --git a/local_backend/schemas/notes.py b/local_backend/schemas/notes.py
new file mode 100644
index 0000000..b828c0c
--- /dev/null
+++ b/local_backend/schemas/notes.py
@@ -0,0 +1,51 @@
+from pydantic import BaseModel
+from typing import Optional
+from datetime import datetime
+
+
+class NoteCreate(BaseModel):
+ body: str
+ is_pinned: bool = False
+
+
+class NoteUpdate(BaseModel):
+ body: Optional[str] = None
+ is_pinned: Optional[bool] = None
+
+
+class NoteOut(BaseModel):
+ id: int
+ body: str
+ is_pinned: bool
+ created_by_id: int
+ created_by_name: Optional[str] = None
+ created_at: datetime
+ updated_at: datetime
+
+ model_config = {"from_attributes": True}
+
+
+class TodoCreate(BaseModel):
+ body: str
+ priority: str = "normal"
+
+
+class TodoUpdate(BaseModel):
+ body: Optional[str] = None
+ priority: Optional[str] = None
+ is_done: Optional[bool] = None
+
+
+class TodoOut(BaseModel):
+ id: int
+ body: str
+ is_done: bool
+ done_at: Optional[datetime] = None
+ done_by_id: Optional[int] = None
+ done_by_name: Optional[str] = None
+ created_by_id: int
+ created_by_name: Optional[str] = None
+ created_at: datetime
+ priority: str
+
+ model_config = {"from_attributes": True}
diff --git a/manager_dashboard/src/App.jsx b/manager_dashboard/src/App.jsx
index 7b4379e..8c2e55d 100644
--- a/manager_dashboard/src/App.jsx
+++ b/manager_dashboard/src/App.jsx
@@ -11,6 +11,7 @@ import ManagementPage from './pages/ManagementPage'
import ReportsPage from './pages/reports/ReportsPage'
import SettingsPage from './pages/Settings/SettingsPage'
import OnlineOrdersPage from './pages/OnlineOrdersPage'
+import NotesPage from './pages/NotesPage'
import client from './api/client'
function Spinner() {
@@ -81,6 +82,7 @@ export default function App() {
{ if (!todo.is_done) { setEditing(true); setDraft(todo.body) } }} + style={{ + margin: 0, fontSize: 13.5, lineHeight: 1.5, wordBreak: 'break-word', + color: todo.is_done ? '#9ca3af' : '#111827', + textDecoration: todo.is_done ? 'line-through' : 'none', + cursor: todo.is_done ? 'default' : 'text', + }} + > + {todo.priority === 'high' && !todo.is_done && ( + ● ΕΠΕΙΓΟΝ + )} + {todo.body} +
+ )} +