From 81577ac7e8f80a50bcc321a031385a370379f6ca Mon Sep 17 00:00:00 2001
From: mohamad <mohamad>
Date: Fri, 23 May 2025 21:01:37 +0200
Subject: [PATCH] feat: Add Recurrence Pattern and Update Expense Schema

- Introduced a new `RecurrencePattern` model to manage recurrence details for expenses, allowing for daily, weekly, monthly, and yearly patterns.
- Updated the `Expense` model to include fields for recurrence management, such as `is_recurring`, `recurrence_pattern_id`, and `next_occurrence`.
- Modified the database schema to reflect these changes, including alterations to existing columns and the removal of obsolete fields.
- Enhanced the expense creation logic to accommodate recurring expenses and updated related CRUD operations accordingly.
- Implemented necessary migrations to ensure database integrity and support for the new features.
---
 .../295cb070f266_add_recurrence_pattern.py    |   90 ++
 be/app/api/v1/endpoints/groups.py             |   13 +-
 be/app/core/scheduler.py                      |    8 +-
 be/app/crud/chore.py                          |    2 +-
 be/app/crud/expense.py                        |    2 +-
 be/app/crud/group.py                          |   29 +-
 be/app/db/__init__.py                         |    3 +
 be/app/db/session.py                          |    4 +
 be/app/jobs/recurring_expenses.py             |    5 +-
 be/app/models.py                              |   39 +
 be/app/schemas/chore.py                       |    8 +-
 fe/package-lock.json                          |    8 +
 fe/package.json                               |    1 +
 fe/src/pages/ChoresPage.vue                   | 1077 +++++++++++++----
 fe/src/pages/GroupDetailPage.vue              |  160 +++
 15 files changed, 1200 insertions(+), 249 deletions(-)
 create mode 100644 be/alembic/versions/295cb070f266_add_recurrence_pattern.py
 create mode 100644 be/app/db/__init__.py
 create mode 100644 be/app/db/session.py

diff --git a/be/alembic/versions/295cb070f266_add_recurrence_pattern.py b/be/alembic/versions/295cb070f266_add_recurrence_pattern.py
new file mode 100644
index 0000000..31fb047
--- /dev/null
+++ b/be/alembic/versions/295cb070f266_add_recurrence_pattern.py
@@ -0,0 +1,90 @@
+"""add_recurrence_pattern
+
+Revision ID: 295cb070f266
+Revises: 7cc1484074eb
+Create Date: 2025-05-22 19:55:24.650524
+
+"""
+from typing import Sequence, Union
+
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision: str = '295cb070f266'
+down_revision: Union[str, None] = '7cc1484074eb'
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+    """Upgrade schema."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.alter_column('expenses', 'next_occurrence',
+               existing_type=postgresql.TIMESTAMP(),
+               type_=sa.DateTime(timezone=True),
+               existing_nullable=True)
+    op.drop_index('ix_expenses_recurring_next_occurrence', table_name='expenses', postgresql_where='(is_recurring = true)')
+    op.drop_constraint('fk_expenses_recurrence_pattern_id', 'expenses', type_='foreignkey')
+    op.drop_constraint('fk_expenses_parent_expense_id', 'expenses', type_='foreignkey')
+    op.drop_column('expenses', 'recurrence_pattern_id')
+    op.drop_column('expenses', 'last_occurrence')
+    op.drop_column('expenses', 'parent_expense_id')
+    op.alter_column('recurrence_patterns', 'days_of_week',
+               existing_type=postgresql.JSON(astext_type=sa.Text()),
+               type_=sa.String(),
+               existing_nullable=True)
+    op.alter_column('recurrence_patterns', 'end_date',
+               existing_type=postgresql.TIMESTAMP(),
+               type_=sa.DateTime(timezone=True),
+               existing_nullable=True)
+    op.alter_column('recurrence_patterns', 'created_at',
+               existing_type=postgresql.TIMESTAMP(),
+               type_=sa.DateTime(timezone=True),
+               existing_nullable=False)
+    op.alter_column('recurrence_patterns', 'updated_at',
+               existing_type=postgresql.TIMESTAMP(),
+               type_=sa.DateTime(timezone=True),
+               existing_nullable=False)
+    op.create_index(op.f('ix_settlement_activities_created_by_user_id'), 'settlement_activities', ['created_by_user_id'], unique=False)
+    op.create_index(op.f('ix_settlement_activities_expense_split_id'), 'settlement_activities', ['expense_split_id'], unique=False)
+    op.create_index(op.f('ix_settlement_activities_id'), 'settlement_activities', ['id'], unique=False)
+    op.create_index(op.f('ix_settlement_activities_paid_by_user_id'), 'settlement_activities', ['paid_by_user_id'], unique=False)
+    # ### end Alembic commands ###
+
+
+def downgrade() -> None:
+    """Downgrade schema."""
+    # ### commands auto generated by Alembic - please adjust! ###
+    op.drop_index(op.f('ix_settlement_activities_paid_by_user_id'), table_name='settlement_activities')
+    op.drop_index(op.f('ix_settlement_activities_id'), table_name='settlement_activities')
+    op.drop_index(op.f('ix_settlement_activities_expense_split_id'), table_name='settlement_activities')
+    op.drop_index(op.f('ix_settlement_activities_created_by_user_id'), table_name='settlement_activities')
+    op.alter_column('recurrence_patterns', 'updated_at',
+               existing_type=sa.DateTime(timezone=True),
+               type_=postgresql.TIMESTAMP(),
+               existing_nullable=False)
+    op.alter_column('recurrence_patterns', 'created_at',
+               existing_type=sa.DateTime(timezone=True),
+               type_=postgresql.TIMESTAMP(),
+               existing_nullable=False)
+    op.alter_column('recurrence_patterns', 'end_date',
+               existing_type=sa.DateTime(timezone=True),
+               type_=postgresql.TIMESTAMP(),
+               existing_nullable=True)
+    op.alter_column('recurrence_patterns', 'days_of_week',
+               existing_type=sa.String(),
+               type_=postgresql.JSON(astext_type=sa.Text()),
+               existing_nullable=True)
+    op.add_column('expenses', sa.Column('parent_expense_id', sa.INTEGER(), autoincrement=False, nullable=True))
+    op.add_column('expenses', sa.Column('last_occurrence', postgresql.TIMESTAMP(), autoincrement=False, nullable=True))
+    op.add_column('expenses', sa.Column('recurrence_pattern_id', sa.INTEGER(), autoincrement=False, nullable=True))
+    op.create_foreign_key('fk_expenses_parent_expense_id', 'expenses', 'expenses', ['parent_expense_id'], ['id'], ondelete='SET NULL')
+    op.create_foreign_key('fk_expenses_recurrence_pattern_id', 'expenses', 'recurrence_patterns', ['recurrence_pattern_id'], ['id'], ondelete='SET NULL')
+    op.create_index('ix_expenses_recurring_next_occurrence', 'expenses', ['is_recurring', 'next_occurrence'], unique=False, postgresql_where='(is_recurring = true)')
+    op.alter_column('expenses', 'next_occurrence',
+               existing_type=sa.DateTime(timezone=True),
+               type_=postgresql.TIMESTAMP(),
+               existing_nullable=True)
+    # ### end Alembic commands ###
diff --git a/be/app/api/v1/endpoints/groups.py b/be/app/api/v1/endpoints/groups.py
index cbef763..185292a 100644
--- a/be/app/api/v1/endpoints/groups.py
+++ b/be/app/api/v1/endpoints/groups.py
@@ -172,22 +172,23 @@ async def leave_group(
     db: AsyncSession = Depends(get_transactional_session),
     current_user: UserModel = Depends(current_active_user),
 ):
-    """Removes the current user from the specified group."""
+    """Removes the current user from the specified group. If the owner is the last member, the group will be deleted."""
     logger.info(f"User {current_user.email} attempting to leave group {group_id}")
     user_role = await crud_group.get_user_role_in_group(db, group_id=group_id, user_id=current_user.id)
 
     if user_role is None:
         raise GroupMembershipError(group_id, "leave (you are not a member)")
 
-    # --- MVP: Prevent owner leaving if they are the last member/owner ---
+    # Check if owner is the last member
     if user_role == UserRoleEnum.owner:
         member_count = await crud_group.get_group_member_count(db, group_id)
-        # More robust check: count owners. For now, just check member count.
         if member_count <= 1:
-            logger.warning(f"Owner {current_user.email} attempted to leave group {group_id} as last member.")
-            raise GroupValidationError("Owner cannot leave the group as the last member. Delete the group or transfer ownership.")
+            # Delete the group since owner is the last member
+            logger.info(f"Owner {current_user.email} is the last member. Deleting group {group_id}")
+            await crud_group.delete_group(db, group_id)
+            return Message(detail="Group deleted as you were the last member")
 
-    # Proceed with removal
+    # Proceed with removal for non-owner or if there are other members
     deleted = await crud_group.remove_user_from_group(db, group_id=group_id, user_id=current_user.id)
 
     if not deleted:
diff --git a/be/app/core/scheduler.py b/be/app/core/scheduler.py
index 89eb9ae..9227cf2 100644
--- a/be/app/core/scheduler.py
+++ b/be/app/core/scheduler.py
@@ -3,16 +3,20 @@ from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
 from apscheduler.executors.pool import ThreadPoolExecutor
 from apscheduler.triggers.cron import CronTrigger
 from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
-from app.core.config import settings
+from app.config import settings
 from app.jobs.recurring_expenses import generate_recurring_expenses
 from app.db.session import async_session
 import logging
 
 logger = logging.getLogger(__name__)
 
+# Convert async database URL to sync URL for APScheduler
+# Replace postgresql+asyncpg:// with postgresql://
+sync_db_url = settings.DATABASE_URL.replace('postgresql+asyncpg://', 'postgresql://')
+
 # Configure the scheduler
 jobstores = {
-    'default': SQLAlchemyJobStore(url=settings.SQLALCHEMY_DATABASE_URI)
+    'default': SQLAlchemyJobStore(url=sync_db_url)
 }
 
 executors = {
diff --git a/be/app/crud/chore.py b/be/app/crud/chore.py
index f6a5ea6..a7e1540 100644
--- a/be/app/crud/chore.py
+++ b/be/app/crud/chore.py
@@ -34,7 +34,7 @@ async def create_chore(
             raise ValueError("group_id must be None for personal chores")
 
     db_chore = Chore(
-        **chore_in.model_dump(exclude_unset=True),
+        **chore_in.model_dump(exclude_unset=True, exclude={'group_id'}),
         group_id=group_id,
         created_by_id=user_id,
     )
diff --git a/be/app/crud/expense.py b/be/app/crud/expense.py
index 10f7627..5af9656 100644
--- a/be/app/crud/expense.py
+++ b/be/app/crud/expense.py
@@ -19,7 +19,6 @@ from app.models import (
     Item as ItemModel,
     ExpenseOverallStatusEnum, # Added
     ExpenseSplitStatusEnum,   # Added
-    RecurrencePattern,
 )
 from app.schemas.expense import ExpenseCreate, ExpenseSplitCreate, ExpenseUpdate # Removed unused ExpenseUpdate
 from app.core.exceptions import (
@@ -34,6 +33,7 @@ from app.core.exceptions import (
     DatabaseTransactionError,# Added
     ExpenseOperationError    # Added specific exception
 )
+from app.models import RecurrencePattern
 
 # Placeholder for InvalidOperationError if not defined in app.core.exceptions
 # This should be a proper HTTPException subclass if used in API layer
diff --git a/be/app/crud/group.py b/be/app/crud/group.py
index 5df3e4a..aea3773 100644
--- a/be/app/crud/group.py
+++ b/be/app/crud/group.py
@@ -267,4 +267,31 @@ async def check_user_role_in_group(
             action=f"{action} (requires at least '{required_role.value}' role)"
         )
     # If role is sufficient, return None
-    return None
\ No newline at end of file
+    return None
+
+async def delete_group(db: AsyncSession, group_id: int) -> None:
+    """
+    Deletes a group and all its associated data (members, invites, lists, etc.).
+    The cascade delete in the models will handle the deletion of related records.
+    
+    Raises:
+        GroupNotFoundError: If the group doesn't exist.
+        DatabaseError: If there's an error during deletion.
+    """
+    try:
+        # Get the group first to ensure it exists
+        group = await get_group_by_id(db, group_id)
+        if not group:
+            raise GroupNotFoundError(group_id)
+
+        # Delete the group - cascading delete will handle related records
+        await db.delete(group)
+        await db.flush()
+        
+        logger.info(f"Group {group_id} deleted successfully")
+    except OperationalError as e:
+        logger.error(f"Database connection error while deleting group {group_id}: {str(e)}", exc_info=True)
+        raise DatabaseConnectionError(f"Database connection error: {str(e)}")
+    except SQLAlchemyError as e:
+        logger.error(f"Unexpected SQLAlchemy error while deleting group {group_id}: {str(e)}", exc_info=True)
+        raise DatabaseTransactionError(f"Failed to delete group: {str(e)}")
\ No newline at end of file
diff --git a/be/app/db/__init__.py b/be/app/db/__init__.py
new file mode 100644
index 0000000..f83198a
--- /dev/null
+++ b/be/app/db/__init__.py
@@ -0,0 +1,3 @@
+from app.db.session import async_session
+
+__all__ = ["async_session"] 
\ No newline at end of file
diff --git a/be/app/db/session.py b/be/app/db/session.py
new file mode 100644
index 0000000..959b962
--- /dev/null
+++ b/be/app/db/session.py
@@ -0,0 +1,4 @@
+from app.database import AsyncSessionLocal
+
+# Export the async session factory
+async_session = AsyncSessionLocal 
\ No newline at end of file
diff --git a/be/app/jobs/recurring_expenses.py b/be/app/jobs/recurring_expenses.py
index c5381d8..96f9026 100644
--- a/be/app/jobs/recurring_expenses.py
+++ b/be/app/jobs/recurring_expenses.py
@@ -4,7 +4,10 @@ from sqlalchemy import select, and_
 from app.models import Expense, RecurrencePattern
 from app.crud.expense import create_expense
 from app.schemas.expense import ExpenseCreate
-from app.core.logging import logger
+import logging
+from typing import Optional
+
+logger = logging.getLogger(__name__)
 
 async def generate_recurring_expenses(db: AsyncSession) -> None:
     """
diff --git a/be/app/models.py b/be/app/models.py
index 182a4ad..74fbba1 100644
--- a/be/app/models.py
+++ b/be/app/models.py
@@ -50,6 +50,13 @@ class ExpenseOverallStatusEnum(enum.Enum):
     partially_paid = "partially_paid"
     paid = "paid"
 
+class RecurrenceTypeEnum(enum.Enum):
+    DAILY = "DAILY"
+    WEEKLY = "WEEKLY"
+    MONTHLY = "MONTHLY"
+    YEARLY = "YEARLY"
+    # Add more types as needed
+
 # Define ChoreFrequencyEnum
 class ChoreFrequencyEnum(enum.Enum):
     one_time = "one_time"
@@ -245,6 +252,11 @@ class Expense(Base):
     item = relationship("Item", foreign_keys=[item_id], back_populates="expenses")
     splits = relationship("ExpenseSplit", back_populates="expense", cascade="all, delete-orphan")
     overall_settlement_status = Column(SAEnum(ExpenseOverallStatusEnum, name="expenseoverallstatusenum", create_type=True), nullable=False, server_default=ExpenseOverallStatusEnum.unpaid.value, default=ExpenseOverallStatusEnum.unpaid)
+    # --- Recurrence fields ---
+    is_recurring = Column(Boolean, default=False, nullable=False)
+    recurrence_pattern_id = Column(Integer, ForeignKey("recurrence_patterns.id"), nullable=True)
+    recurrence_pattern = relationship("RecurrencePattern", back_populates="expenses", uselist=False) # One-to-one
+    next_occurrence = Column(DateTime(timezone=True), nullable=True) # For recurring expenses
 
     __table_args__ = (
         # Ensure at least one context is provided
@@ -376,3 +388,30 @@ class ChoreAssignment(Base):
     # --- Relationships ---
     chore = relationship("Chore", back_populates="assignments")
     assigned_user = relationship("User", back_populates="assigned_chores")
+
+
+# === NEW: RecurrencePattern Model ===
+class RecurrencePattern(Base):
+    __tablename__ = "recurrence_patterns"
+
+    id = Column(Integer, primary_key=True, index=True)
+    type = Column(SAEnum(RecurrenceTypeEnum, name="recurrencetypeenum", create_type=True), nullable=False)
+    interval = Column(Integer, default=1, nullable=False)  # e.g., every 1 day, every 2 weeks
+    days_of_week = Column(String, nullable=True)  # For weekly recurrences, e.g., "MON,TUE,FRI"
+    # day_of_month = Column(Integer, nullable=True) # For monthly on a specific day
+    # week_of_month = Column(Integer, nullable=True) # For monthly on a specific week (e.g., 2nd week)
+    # month_of_year = Column(Integer, nullable=True) # For yearly recurrences
+    end_date = Column(DateTime(timezone=True), nullable=True)
+    max_occurrences = Column(Integer, nullable=True)
+
+    created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
+    updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
+
+    # Relationship back to Expenses that use this pattern (could be one-to-many if patterns are shared)
+    # However, the current CRUD implies one RecurrencePattern per Expense if recurring.
+    # If a pattern can be shared, this would be a one-to-many (RecurrencePattern to many Expenses).
+    # For now, assuming one-to-one as implied by current Expense.recurrence_pattern relationship setup.
+    expenses = relationship("Expense", back_populates="recurrence_pattern")
+
+
+# === END: RecurrencePattern Model ===
diff --git a/be/app/schemas/chore.py b/be/app/schemas/chore.py
index da316fa..7ba70f1 100644
--- a/be/app/schemas/chore.py
+++ b/be/app/schemas/chore.py
@@ -42,9 +42,9 @@ class ChoreCreate(ChoreBase):
     @field_validator('group_id')
     @classmethod
     def validate_group_id(cls, v, values):
-        if values.get('type') == ChoreTypeEnum.group and v is None:
+        if values.data.get('type') == ChoreTypeEnum.group and v is None:
             raise ValueError("group_id is required for group chores")
-        if values.get('type') == ChoreTypeEnum.personal and v is not None:
+        if values.data.get('type') == ChoreTypeEnum.personal and v is not None:
             raise ValueError("group_id must be None for personal chores")
         return v
 
@@ -61,9 +61,9 @@ class ChoreUpdate(BaseModel):
     @field_validator('group_id')
     @classmethod
     def validate_group_id(cls, v, values):
-        if values.get('type') == ChoreTypeEnum.group and v is None:
+        if values.data.get('type') == ChoreTypeEnum.group and v is None:
             raise ValueError("group_id is required for group chores")
-        if values.get('type') == ChoreTypeEnum.personal and v is not None:
+        if values.data.get('type') == ChoreTypeEnum.personal and v is not None:
             raise ValueError("group_id must be None for personal chores")
         return v
 
diff --git a/fe/package-lock.json b/fe/package-lock.json
index 7f9a258..13133d2 100644
--- a/fe/package-lock.json
+++ b/fe/package-lock.json
@@ -25,6 +25,7 @@
         "@intlify/unplugin-vue-i18n": "^6.0.8",
         "@playwright/test": "^1.51.1",
         "@tsconfig/node22": "^22.0.1",
+        "@types/date-fns": "^2.5.3",
         "@types/jsdom": "^21.1.7",
         "@types/node": "^22.15.17",
         "@vitejs/plugin-vue": "^5.2.3",
@@ -4124,6 +4125,13 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/@types/date-fns": {
+      "version": "2.5.3",
+      "resolved": "https://registry.npmjs.org/@types/date-fns/-/date-fns-2.5.3.tgz",
+      "integrity": "sha512-4KVPD3g5RjSgZtdOjvI/TDFkLNUHhdoWxmierdQbDeEg17Rov0hbBYtIzNaQA67ORpteOhvR9YEMTb6xeDCang==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/@types/estree": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
diff --git a/fe/package.json b/fe/package.json
index 272c4ed..0d9febe 100644
--- a/fe/package.json
+++ b/fe/package.json
@@ -34,6 +34,7 @@
     "@intlify/unplugin-vue-i18n": "^6.0.8",
     "@playwright/test": "^1.51.1",
     "@tsconfig/node22": "^22.0.1",
+    "@types/date-fns": "^2.5.3",
     "@types/jsdom": "^21.1.7",
     "@types/node": "^22.15.17",
     "@vitejs/plugin-vue": "^5.2.3",
diff --git a/fe/src/pages/ChoresPage.vue b/fe/src/pages/ChoresPage.vue
index fd26ee1..5b5ace0 100644
--- a/fe/src/pages/ChoresPage.vue
+++ b/fe/src/pages/ChoresPage.vue
@@ -1,118 +1,301 @@
 <template>
   <main class="container page-padding">
-    <div class="row q-mb-md items-center justify-between">
-      <h1 class="mb-3">All Chores</h1>
+    <div class="page-header">
+      <h1 class="mb-3">Chores Timeline</h1>
       <button class="btn btn-primary" @click="openCreateChoreModal(null)">
         <span class="material-icons">add</span>
         New Chore
       </button>
     </div>
 
-    <!-- Chores List -->
-    <div v-if="groupedChores.personal.length > 0">
-      <h2 class="chores-group-title">Personal Chores</h2>
-      <div class="neo-grid">
-        <div v-for="chore in groupedChores.personal" :key="chore.id" class="neo-card">
-          <div class="neo-card-header">
-            <div class="row items-center justify-between">
-              <h3>{{ chore.name }}</h3>
-              <span class="neo-chore-frequency" :class="chore.frequency">
-                {{ formatFrequency(chore.frequency) }}
-              </span>
+    <!-- Timeline View -->
+    <div v-if="allChores.length > 0" class="chores-timeline">
+      <!-- Overdue Section -->
+      <div v-if="choresByTimeline.overdue.length > 0" class="timeline-section overdue">
+        <div class="timeline-header">
+          <div class="timeline-dot overdue"></div>
+          <h2 class="timeline-title">Overdue</h2>
+          <span class="timeline-count">{{ choresByTimeline.overdue.length }}</span>
+        </div>
+        <div class="timeline-items">
+          <div v-for="chore in choresByTimeline.overdue" :key="chore.id" class="timeline-chore-card overdue">
+            <div class="chore-timeline-marker"></div>
+            <div class="chore-content">
+              <div class="chore-header">
+                <h3>{{ chore.name }}</h3>
+                <div class="chore-tags">
+                  <span class="chore-type-tag" :class="chore.type">
+                    {{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
+                  </span>
+                  <span class="chore-frequency-tag" :class="chore.frequency">
+                    {{ formatFrequency(chore.frequency) }}
+                  </span>
+                </div>
+              </div>
+              <div class="chore-meta">
+                <div class="chore-due-date overdue">
+                  <span class="material-icons">schedule</span>
+                  Due {{ formatDate(chore.next_due_date) }}
+                </div>
+                <div v-if="chore.description" class="chore-description">
+                  {{ chore.description }}
+                </div>
+              </div>
+              <div class="chore-actions">
+                <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
+                  <span class="material-icons">edit</span>
+                  Edit
+                </button>
+                <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
+                  <span class="material-icons">delete</span>
+                  Delete
+                </button>
+              </div>
             </div>
           </div>
-          <div class="neo-card-body">
-            <div class="neo-chore-info">
-              <div class="neo-chore-due">
-                Due: {{ formatDate(chore.next_due_date) }}
+        </div>
+      </div>
+
+      <!-- Today Section -->
+      <div v-if="choresByTimeline.today.length > 0" class="timeline-section today">
+        <div class="timeline-header">
+          <div class="timeline-dot today"></div>
+          <h2 class="timeline-title">Today</h2>
+          <span class="timeline-count">{{ choresByTimeline.today.length }}</span>
+        </div>
+        <div class="timeline-items">
+          <div v-for="chore in choresByTimeline.today" :key="chore.id" class="timeline-chore-card today">
+            <div class="chore-timeline-marker"></div>
+            <div class="chore-content">
+              <div class="chore-header">
+                <h3>{{ chore.name }}</h3>
+                <div class="chore-tags">
+                  <span class="chore-type-tag" :class="chore.type">
+                    {{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
+                  </span>
+                  <span class="chore-frequency-tag" :class="chore.frequency">
+                    {{ formatFrequency(chore.frequency) }}
+                  </span>
+                </div>
               </div>
-              <div v-if="chore.description" class="neo-chore-description">
-                {{ chore.description }}
+              <div class="chore-meta">
+                <div class="chore-due-date today">
+                  <span class="material-icons">today</span>
+                  Due Today
+                </div>
+                <div v-if="chore.description" class="chore-description">
+                  {{ chore.description }}
+                </div>
+              </div>
+              <div class="chore-actions">
+                <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
+                  <span class="material-icons">edit</span>
+                  Edit
+                </button>
+                <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
+                  <span class="material-icons">delete</span>
+                  Delete
+                </button>
               </div>
             </div>
-            <div class="neo-card-actions">
-              <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
-                <span class="material-icons">edit</span>
-                Edit
-              </button>
-              <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
-                <span class="material-icons">delete</span>
-                Delete
-              </button>
+          </div>
+        </div>
+      </div>
+
+      <!-- Tomorrow Section -->
+      <div v-if="choresByTimeline.tomorrow.length > 0" class="timeline-section tomorrow">
+        <div class="timeline-header">
+          <div class="timeline-dot tomorrow"></div>
+          <h2 class="timeline-title">Tomorrow</h2>
+          <span class="timeline-count">{{ choresByTimeline.tomorrow.length }}</span>
+        </div>
+        <div class="timeline-items">
+          <div v-for="chore in choresByTimeline.tomorrow" :key="chore.id" class="timeline-chore-card tomorrow">
+            <div class="chore-timeline-marker"></div>
+            <div class="chore-content">
+              <div class="chore-header">
+                <h3>{{ chore.name }}</h3>
+                <div class="chore-tags">
+                  <span class="chore-type-tag" :class="chore.type">
+                    {{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
+                  </span>
+                  <span class="chore-frequency-tag" :class="chore.frequency">
+                    {{ formatFrequency(chore.frequency) }}
+                  </span>
+                </div>
+              </div>
+              <div class="chore-meta">
+                <div class="chore-due-date tomorrow">
+                  <span class="material-icons">event</span>
+                  Due Tomorrow
+                </div>
+                <div v-if="chore.description" class="chore-description">
+                  {{ chore.description }}
+                </div>
+              </div>
+              <div class="chore-actions">
+                <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
+                  <span class="material-icons">edit</span>
+                  Edit
+                </button>
+                <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
+                  <span class="material-icons">delete</span>
+                  Delete
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- This Week Section -->
+      <div v-if="choresByTimeline.thisWeek.length > 0" class="timeline-section this-week">
+        <div class="timeline-header">
+          <div class="timeline-dot this-week"></div>
+          <h2 class="timeline-title">This Week</h2>
+          <span class="timeline-count">{{ choresByTimeline.thisWeek.length }}</span>
+        </div>
+        <div class="timeline-items">
+          <div v-for="chore in choresByTimeline.thisWeek" :key="chore.id" class="timeline-chore-card this-week">
+            <div class="chore-timeline-marker"></div>
+            <div class="chore-content">
+              <div class="chore-header">
+                <h3>{{ chore.name }}</h3>
+                <div class="chore-tags">
+                  <span class="chore-type-tag" :class="chore.type">
+                    {{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
+                  </span>
+                  <span class="chore-frequency-tag" :class="chore.frequency">
+                    {{ formatFrequency(chore.frequency) }}
+                  </span>
+                </div>
+              </div>
+              <div class="chore-meta">
+                <div class="chore-due-date this-week">
+                  <span class="material-icons">date_range</span>
+                  Due {{ formatDate(chore.next_due_date) }}
+                </div>
+                <div v-if="chore.description" class="chore-description">
+                  {{ chore.description }}
+                </div>
+              </div>
+              <div class="chore-actions">
+                <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
+                  <span class="material-icons">edit</span>
+                  Edit
+                </button>
+                <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
+                  <span class="material-icons">delete</span>
+                  Delete
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Later Section -->
+      <div v-if="choresByTimeline.later.length > 0" class="timeline-section later">
+        <div class="timeline-header">
+          <div class="timeline-dot later"></div>
+          <h2 class="timeline-title">Later</h2>
+          <span class="timeline-count">{{ choresByTimeline.later.length }}</span>
+        </div>
+        <div class="timeline-items">
+          <div v-for="chore in choresByTimeline.later" :key="chore.id" class="timeline-chore-card later">
+            <div class="chore-timeline-marker"></div>
+            <div class="chore-content">
+              <div class="chore-header">
+                <h3>{{ chore.name }}</h3>
+                <div class="chore-tags">
+                  <span class="chore-type-tag" :class="chore.type">
+                    {{ chore.type === 'personal' ? 'Personal' : getGroupName(chore.group_id) || 'Group' }}
+                  </span>
+                  <span class="chore-frequency-tag" :class="chore.frequency">
+                    {{ formatFrequency(chore.frequency) }}
+                  </span>
+                </div>
+              </div>
+              <div class="chore-meta">
+                <div class="chore-due-date later">
+                  <span class="material-icons">schedule</span>
+                  Due {{ formatDate(chore.next_due_date) }}
+                </div>
+                <div v-if="chore.description" class="chore-description">
+                  {{ chore.description }}
+                </div>
+              </div>
+              <div class="chore-actions">
+                <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
+                  <span class="material-icons">edit</span>
+                  Edit
+                </button>
+                <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
+                  <span class="material-icons">delete</span>
+                  Delete
+                </button>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- Add New Chore Section -->
+      <div class="timeline-section add-new">
+        <div class="timeline-header">
+          <div class="timeline-dot add-new"></div>
+          <h2 class="timeline-title">Add New</h2>
+        </div>
+        <div class="timeline-items">
+          <div class="timeline-add-card" @click="openCreateChoreModal(null)">
+            <div class="chore-timeline-marker"></div>
+            <div class="add-content">
+              <span class="material-icons">add_circle</span>
+              <span>Create a new chore</span>
             </div>
           </div>
         </div>
       </div>
     </div>
 
-    <div v-for="group in groupedChores.groups" :key="group.id">
-      <h2 class="chores-group-title">{{ group.name }}</h2>
-      <div class="neo-grid" v-if="group.chores.length > 0">
-        <div v-for="chore in group.chores" :key="chore.id" class="neo-card">
-          <div class="neo-card-header">
-            <div class="row items-center justify-between">
-              <h3>{{ chore.name }}</h3>
-              <span class="neo-chore-frequency" :class="chore.frequency">
-                {{ formatFrequency(chore.frequency) }}
-              </span>
-            </div>
-          </div>
-          <div class="neo-card-body">
-            <div class="neo-chore-info">
-              <div class="neo-chore-due">
-                Due: {{ formatDate(chore.next_due_date) }}
-              </div>
-              <div v-if="chore.description" class="neo-chore-description">
-                {{ chore.description }}
-              </div>
-            </div>
-            <div class="neo-card-actions">
-              <button class="btn btn-primary btn-sm" @click="openEditChoreModal(chore)">
-                <span class="material-icons">edit</span>
-                Edit
-              </button>
-              <button class="btn btn-danger btn-sm" @click="confirmDeleteChore(chore)">
-                <span class="material-icons">delete</span>
-                Delete
-              </button>
-            </div>
-          </div>
-        </div>
-      </div>
-      <p v-else>No chores in this group.</p>
-    </div>
-
-    <div v-if="groupedChores.personal.length === 0 && groupedChores.groups.length === 0">
-      <p>No chores found. Get started by adding a new chore!</p>
+    <div v-else class="card empty-state-card">
+      <svg class="icon icon-lg" aria-hidden="true">
+        <use xlink:href="#icon-clipboard" />
+      </svg>
+      <h3>No Chores Yet!</h3>
+      <p>Get started by adding a new chore to your timeline!</p>
+      <button class="btn btn-primary mt-2" @click="openCreateChoreModal(null)">
+        <svg class="icon" aria-hidden="true">
+          <use xlink:href="#icon-plus" />
+        </svg>
+        Create New Chore
+      </button>
     </div>
 
     <!-- Create/Edit Chore Modal -->
-    <div v-if="showChoreModal" class="neo-modal">
-      <div class="neo-modal-content">
-        <div class="neo-modal-header">
+    <div v-if="showChoreModal" class="modal-backdrop open" @click.self="showChoreModal = false">
+      <div class="modal-container" role="dialog" aria-modal="true">
+        <div class="modal-header">
           <h3>{{ isEditing ? 'Edit Chore' : 'New Chore' }}</h3>
-          <button class="btn btn-neutral btn-icon-only" @click="showChoreModal = false">
-            <span class="material-icons">close</span>
+          <button class="close-button" @click="showChoreModal = false" aria-label="Close">
+            <svg class="icon" aria-hidden="true">
+              <use xlink:href="#icon-close" />
+            </svg>
           </button>
         </div>
-        <div class="neo-modal-body">
-          <form @submit.prevent="onSubmit" class="neo-form">
-            <div class="neo-form-group">
+        <form @submit.prevent="onSubmit" class="neo-form">
+          <div class="modal-body">
+            <div class="form-group">
               <label for="name">Name</label>
-              <input
-                id="name"
-                v-model="choreForm.name"
-                type="text"
-                class="neo-input"
-                required
-              />
+              <input id="name" v-model="choreForm.name" type="text" class="form-input" required />
             </div>
 
-            <div class="neo-form-group">
+            <div class="form-group">
               <label>Chore Type</label>
               <div class="radio-group">
                 <label>
-                  <input type="radio" v-model="choreForm.type" value="personal" @change="choreForm.group_id = undefined">
+                  <input type="radio" v-model="choreForm.type" value="personal"
+                    @change="choreForm.group_id = undefined">
                   Personal
                 </label>
                 <label>
@@ -122,14 +305,13 @@
               </div>
             </div>
 
-            <div v-if="choreForm.type === 'group'" class="neo-form-group">
+            <div v-if="choreForm.type === 'group'" class="form-group">
               <label for="group">Group</label>
-              <select id="group" v-model="choreForm.group_id" class="neo-input" required>
+              <select id="group" v-model="choreForm.group_id" class="form-input" required>
                 <option :value="undefined" disabled>Select a group</option>
                 <option v-for="group in groups" :key="group.id" :value="group.id">
                   {{ group.name }}
                 </option>
-                <!-- Placeholder if no groups loaded yet -->
                 <option v-if="groups.length === 0 && choreForm.group_id" :value="choreForm.group_id" disabled>
                   Group ID: {{ choreForm.group_id }} (Loading groups...)
                 </option>
@@ -139,76 +321,56 @@
               </p>
             </div>
 
-            <div class="neo-form-group">
+            <div class="form-group">
               <label for="description">Description</label>
-              <textarea
-                id="description"
-                v-model="choreForm.description"
-                class="neo-input"
-                rows="3"
-              ></textarea>
+              <textarea id="description" v-model="choreForm.description" class="form-input" rows="3"></textarea>
             </div>
 
-            <div class="neo-form-group">
+            <div class="form-group">
               <label for="frequency">Frequency</label>
-              <select
-                id="frequency"
-                v-model="choreForm.frequency"
-                class="neo-input"
-                required
-              >
+              <select id="frequency" v-model="choreForm.frequency" class="form-input" required>
                 <option v-for="option in frequencyOptions" :key="option.value" :value="option.value">
                   {{ option.label }}
                 </option>
               </select>
             </div>
 
-            <div v-if="choreForm.frequency === 'custom'" class="neo-form-group">
+            <div v-if="choreForm.frequency === 'custom'" class="form-group">
               <label for="interval">Interval (days)</label>
-              <input
-                id="interval"
-                v-model.number="choreForm.custom_interval_days"
-                type="number"
-                class="neo-input"
-                min="1"
-                required
-              />
+              <input id="interval" v-model.number="choreForm.custom_interval_days" type="number" class="form-input"
+                min="1" required />
             </div>
 
-            <div class="neo-form-group">
+            <div class="form-group">
               <label for="dueDate">Next Due Date</label>
-              <input
-                id="dueDate"
-                v-model="choreForm.next_due_date"
-                type="date"
-                class="neo-input"
-                required
-              />
+              <input id="dueDate" v-model="choreForm.next_due_date" type="date" class="form-input" required />
             </div>
-          </form>
-        </div>
-        <div class="neo-modal-footer">
-          <button class="btn btn-neutral" @click="showChoreModal = false">Cancel</button>
-          <button class="btn btn-primary" @click="onSubmit">Save</button>
-        </div>
+          </div>
+          <div class="modal-footer">
+            <button type="button" class="btn btn-neutral" @click="showChoreModal = false">Cancel</button>
+            <button type="submit" class="btn btn-primary ml-2">Save</button>
+          </div>
+        </form>
       </div>
     </div>
 
     <!-- Delete Confirmation Dialog -->
-    <div v-if="showDeleteDialog" class="neo-modal">
-      <div class="neo-modal-content">
-        <div class="neo-modal-header">
+    <div v-if="showDeleteDialog" class="modal-backdrop open" @click.self="showDeleteDialog = false">
+      <div class="modal-container" role="dialog" aria-modal="true">
+        <div class="modal-header">
           <h3>Delete Chore</h3>
-          <button class="btn btn-neutral btn-icon-only" @click="showDeleteDialog = false">
-            <span class="material-icons">close</span>
+          <button class="close-button" @click="showDeleteDialog = false" aria-label="Close">
+            <svg class="icon" aria-hidden="true">
+              <use xlink:href="#icon-close" />
+            </svg>
           </button>
         </div>
-        <div class="neo-modal-body">
+        <div class="modal-body">
           <p>Are you sure you want to delete this chore?</p>
         </div>
-        <div class="neo-modal-footer">
+        <div class="modal-footer">
           <button class="btn btn-neutral" @click="showDeleteDialog = false">Cancel</button>
-          <button class="btn btn-danger" @click="deleteChore">Delete</button>
+          <button class="btn btn-danger ml-2" @click="deleteChore">Delete</button>
         </div>
       </div>
     </div>
@@ -222,18 +384,33 @@ import { choreService } from '../services/choreService'
 import { useNotificationStore } from '../stores/notifications'
 import type { Chore, ChoreCreate, ChoreUpdate, ChoreFrequency, ChoreType } from '../types/chore'
 import { useRoute } from 'vue-router'
+import { groupService } from '../services/groupService'
+import { useStorage } from '@vueuse/core'
 
 const notificationStore = useNotificationStore()
 const route = useRoute()
 
 // State
 const chores = ref<Chore[]>([])
-const groups = ref<{id: number, name: string}[]>([]) // To store group info
+const groups = ref<{ id: number, name: string }[]>([]) // To store group info
 const showChoreModal = ref(false)
 const showDeleteDialog = ref(false)
 const isEditing = ref(false)
 const selectedChore = ref<Chore | null>(null)
 
+// Cache chores in localStorage
+const cachedChores = useStorage<Chore[]>('cached-chores', [])
+const cachedTimestamp = useStorage<number>('cached-chores-timestamp', 0)
+const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes in milliseconds
+
+// Load cached data immediately if available and not expired
+const loadCachedData = () => {
+  const now = Date.now();
+  if (cachedChores.value.length > 0 && (now - cachedTimestamp.value) < CACHE_DURATION) {
+    chores.value = cachedChores.value;
+  }
+};
+
 const choreForm = ref<ChoreCreate>({
   name: '',
   description: '',
@@ -273,6 +450,67 @@ const groupedChores = computed(() => {
   };
 });
 
+// Add computed property for all chores in a single array
+const allChores = computed(() => {
+  return chores.value.sort((a, b) => {
+    // Sort by due date, then by name
+    const dateA = new Date(a.next_due_date);
+    const dateB = new Date(b.next_due_date);
+    if (dateA.getTime() !== dateB.getTime()) {
+      return dateA.getTime() - dateB.getTime();
+    }
+    return a.name.localeCompare(b.name);
+  });
+});
+
+// Add helper function to get group name
+const getGroupName = (groupId?: number | null): string | undefined => {
+  if (!groupId) return undefined;
+  return groups.value.find(g => g.id === groupId)?.name;
+};
+
+// Add computed property for timeline grouping
+const choresByTimeline = computed(() => {
+  const now = new Date();
+  const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+  const tomorrow = new Date(today);
+  tomorrow.setDate(tomorrow.getDate() + 1);
+  const nextWeek = new Date(today);
+  nextWeek.setDate(nextWeek.getDate() + 7);
+
+  const timeline = {
+    overdue: [] as Chore[],
+    today: [] as Chore[],
+    tomorrow: [] as Chore[],
+    thisWeek: [] as Chore[],
+    later: [] as Chore[]
+  };
+
+  chores.value.forEach(chore => {
+    const dueDate = new Date(chore.next_due_date);
+    const choreDate = new Date(dueDate.getFullYear(), dueDate.getMonth(), dueDate.getDate());
+
+    if (choreDate < today) {
+      timeline.overdue.push(chore);
+    } else if (choreDate.getTime() === today.getTime()) {
+      timeline.today.push(chore);
+    } else if (choreDate.getTime() === tomorrow.getTime()) {
+      timeline.tomorrow.push(chore);
+    } else if (choreDate < nextWeek) {
+      timeline.thisWeek.push(chore);
+    } else {
+      timeline.later.push(chore);
+    }
+  });
+
+  // Sort each timeline section by name
+  Object.values(timeline).forEach(section => {
+    section.sort((a, b) => a.name.localeCompare(b.name));
+  });
+
+  return timeline;
+});
+
 const frequencyOptions = [
   { label: 'One Time', value: 'one_time' as ChoreFrequency },
   { label: 'Daily', value: 'daily' as ChoreFrequency },
@@ -286,12 +524,20 @@ const loadChores = async () => {
   try {
     // Use the new unified service method
     chores.value = await choreService.getAllChores()
+
+    // Update cache
+    cachedChores.value = chores.value
+    cachedTimestamp.value = Date.now()
   } catch (error) {
     console.error('Failed to load all chores:', error)
     notificationStore.addNotification({
       message: 'Failed to load chores',
       type: 'error'
     })
+    // If we have cached data, keep showing it even if refresh failed
+    if (cachedChores.value.length === 0) {
+      chores.value = []
+    }
   }
 }
 
@@ -324,11 +570,11 @@ const openEditChoreModal = (chore: Chore) => {
   // Reformat next_due_date if it's not already yyyy-MM-dd
   if (chore.next_due_date) {
     try {
-        choreForm.value.next_due_date = format(new Date(chore.next_due_date), 'yyyy-MM-dd');
+      choreForm.value.next_due_date = format(new Date(chore.next_due_date), 'yyyy-MM-dd');
     } catch (e) {
-        console.warn("Could not parse next_due_date for editing:", chore.next_due_date);
-        // Keep original if parsing fails, or set to today as a fallback
-        choreForm.value.next_due_date = format(new Date(), 'yyyy-MM-dd');
+      console.warn("Could not parse next_due_date for editing:", chore.next_due_date);
+      // Keep original if parsing fails, or set to today as a fallback
+      choreForm.value.next_due_date = format(new Date(), 'yyyy-MM-dd');
     }
   }
   showChoreModal.value = true
@@ -362,7 +608,7 @@ const onSubmit = async () => {
     });
 
     showChoreModal.value = false;
-    loadChores(); // Reload all chores
+    loadChores(); // Reload all chores (this will update cache)
   } catch (error) {
     console.error('Failed to save chore:', error);
     notificationStore.addNotification({
@@ -388,7 +634,7 @@ const deleteChore = async () => {
       message: `Chore deleted successfully`,
       type: 'success'
     })
-    loadChores() // Reload all chores
+    loadChores() // Reload all chores (this will update cache)
   } catch (error) {
     console.error('Failed to delete chore:', error);
     notificationStore.addNotification({
@@ -404,7 +650,7 @@ const formatDate = (date: string) => {
   } else if (date) {
     const parts = date.split('-');
     if (parts.length === 3) {
-        return format(new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])), 'MMM d, yyyy');
+      return format(new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2])), 'MMM d, yyyy');
     }
   }
   return 'Invalid Date';
@@ -416,19 +662,23 @@ const formatFrequency = (frequency: ChoreFrequency) => {
 }
 
 const loadGroups = async () => {
-  // Placeholder: In a real scenario, this would call groupService
-  // For now, we can mock it if we want to see the structure, or leave it empty.
-  // groups.value = await groupService.getUserGroups();
-  // Mock example:
-  // groups.value = [
-  //   { id: 1, name: 'Family' },
-  //   { id: 2, name: 'Work Team' }
-  // ];
-  console.log('loadGroups called - placeholder for fetching groups');
+  try {
+    groups.value = await groupService.getUserGroups();
+  } catch (error) {
+    console.error('Failed to load groups:', error);
+    notificationStore.addNotification({
+      message: 'Failed to load groups',
+      type: 'error'
+    });
+  }
 };
 
 // Lifecycle
 onMounted(() => {
+  // Load cached data immediately
+  loadCachedData()
+
+  // Then fetch fresh data in background
   loadChores()
   loadGroups() // Call loadGroups
 })
@@ -438,86 +688,371 @@ onMounted(() => {
 <style scoped>
 .page-padding {
   padding: 1rem;
-  max-width: 1200px;
+  max-width: 800px;
   margin: 0 auto;
 }
 
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 2rem;
+}
+
 .mb-3 {
   margin-bottom: 1.5rem;
 }
 
-/* Neo Grid Layout */
-.neo-grid {
-  display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
-  gap: 2rem;
-  margin-bottom: 2rem;
+.mt-2 {
+  margin-top: 1rem;
 }
 
-/* Neo Card Styles */
-.neo-card {
+.ml-2 {
+  margin-left: 0.5rem;
+}
+
+/* Timeline Layout */
+.chores-timeline {
+  position: relative;
+  margin-left: 2rem;
+}
+
+/* Timeline line */
+.chores-timeline::before {
+  content: '';
+  position: absolute;
+  left: -1rem;
+  top: 0;
+  bottom: 0;
+  width: 4px;
+  background: #ddd;
+  border-radius: 2px;
+}
+
+/* Timeline Sections */
+.timeline-section {
+  position: relative;
+  margin-bottom: 2rem;
+  background: var(--light);
   border-radius: 18px;
   box-shadow: 6px 6px 0 #111;
-  background: var(--light);
   border: 3px solid #111;
   overflow: hidden;
 }
 
-.neo-card-header {
-  padding: 1.5rem;
-  border-bottom: 3px solid #111;
-  background: #fafafa;
+.timeline-section.overdue {
+  border-color: #dc3545;
+  box-shadow: 6px 6px 0 #dc3545;
 }
 
-.neo-card-header h3 {
+.timeline-section.today {
+  border-color: #007bff;
+  box-shadow: 6px 6px 0 #007bff;
+}
+
+.timeline-section.tomorrow {
+  border-color: #28a745;
+  box-shadow: 6px 6px 0 #28a745;
+}
+
+.timeline-section.this-week {
+  border-color: #ffc107;
+  box-shadow: 6px 6px 0 #ffc107;
+}
+
+.timeline-section.later {
+  border-color: #6c757d;
+  box-shadow: 6px 6px 0 #6c757d;
+}
+
+.timeline-section.add-new {
+  border: 3px dashed #111;
+  box-shadow: 6px 6px 0 #111;
+}
+
+/* Timeline Header */
+.timeline-header {
+  padding: 1.5rem;
+  border-bottom: 3px solid;
+  background: #fafafa;
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.timeline-section.overdue .timeline-header {
+  border-bottom-color: #dc3545;
+  background: linear-gradient(135deg, #f8d7da 0%, #fafafa 100%);
+}
+
+.timeline-section.today .timeline-header {
+  border-bottom-color: #007bff;
+  background: linear-gradient(135deg, #cce7ff 0%, #fafafa 100%);
+}
+
+.timeline-section.tomorrow .timeline-header {
+  border-bottom-color: #28a745;
+  background: linear-gradient(135deg, #d4edda 0%, #fafafa 100%);
+}
+
+.timeline-section.this-week .timeline-header {
+  border-bottom-color: #ffc107;
+  background: linear-gradient(135deg, #fff3cd 0%, #fafafa 100%);
+}
+
+.timeline-section.later .timeline-header {
+  border-bottom-color: #6c757d;
+  background: linear-gradient(135deg, #e2e3e5 0%, #fafafa 100%);
+}
+
+.timeline-section.add-new .timeline-header {
+  border-bottom-color: #111;
+}
+
+/* Timeline Dots */
+.timeline-dot {
+  position: absolute;
+  left: -2.25rem;
+  top: 1.75rem;
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  border: 4px solid #fff;
+  box-shadow: 0 0 0 3px #111;
+}
+
+.timeline-dot.overdue {
+  background: #dc3545;
+}
+
+.timeline-dot.today {
+  background: #007bff;
+}
+
+.timeline-dot.tomorrow {
+  background: #28a745;
+}
+
+.timeline-dot.this-week {
+  background: #ffc107;
+}
+
+.timeline-dot.later {
+  background: #6c757d;
+}
+
+.timeline-dot.add-new {
+  background: #111;
+}
+
+.timeline-title {
   font-weight: 900;
   font-size: 1.25rem;
   margin: 0;
   letter-spacing: 0.5px;
+  flex: 1;
 }
 
-.neo-card-body {
+.timeline-count {
+  font-size: 0.875rem;
+  color: #666;
+  font-weight: 700;
+  background: rgba(255, 255, 255, 0.8);
+  padding: 0.25rem 0.75rem;
+  border-radius: 1rem;
+  border: 2px solid #111;
+}
+
+/* Timeline Items */
+.timeline-items {
   padding: 1.5rem;
 }
 
-.neo-card-actions {
+.timeline-chore-card {
   display: flex;
   gap: 1rem;
-  margin-top: 1rem;
+  margin-bottom: 1.5rem;
+  padding: 1rem;
+  background: #fff;
+  border-radius: 12px;
+  border: 2px solid #eee;
+  transition: all 0.2s ease;
 }
 
-/* Chore Info Styles */
-.neo-chore-info {
+.timeline-chore-card:hover {
+  border-color: #111;
+  transform: translateX(5px);
+}
+
+.timeline-chore-card:last-child {
+  margin-bottom: 0;
+}
+
+.chore-timeline-marker {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  background: #111;
+  margin-top: 0.75rem;
+  flex-shrink: 0;
+}
+
+.chore-content {
+  flex: 1;
+}
+
+.chore-header {
+  margin-bottom: 0.75rem;
+}
+
+.chore-header h3 {
+  font-weight: 900;
+  font-size: 1.1rem;
+  margin: 0 0 0.5rem 0;
+  letter-spacing: 0.5px;
+  color: #111;
+}
+
+.chore-tags {
   display: flex;
-  flex-direction: column;
   gap: 0.5rem;
+  flex-wrap: wrap;
 }
 
-.neo-chore-due {
+/* Tag Styles */
+.chore-type-tag,
+.chore-frequency-tag {
+  font-size: 0.75rem;
+  padding: 0.25rem 0.5rem;
+  border-radius: 0.75rem;
+  font-weight: 700;
+  text-transform: uppercase;
+  letter-spacing: 0.5px;
+}
+
+.chore-type-tag.personal {
+  background: #d4edda;
+  color: #155724;
+  border: 2px solid #155724;
+}
+
+.chore-type-tag.group {
+  background: #cce7ff;
+  color: #0056b3;
+  border: 2px solid #0056b3;
+}
+
+.chore-frequency-tag.one_time {
+  background: #e0e0e0;
+  color: #333;
+  border: 2px solid #333;
+}
+
+.chore-frequency-tag.daily {
+  background: #bbdefb;
+  color: #1565c0;
+  border: 2px solid #1565c0;
+}
+
+.chore-frequency-tag.weekly {
+  background: #c8e6c9;
+  color: #2e7d32;
+  border: 2px solid #2e7d32;
+}
+
+.chore-frequency-tag.monthly {
+  background: #e1bee7;
+  color: #7b1fa2;
+  border: 2px solid #7b1fa2;
+}
+
+.chore-frequency-tag.custom {
+  background: #ffe0b2;
+  color: #ef6c00;
+  border: 2px solid #ef6c00;
+}
+
+.chore-meta {
+  margin-bottom: 1rem;
+}
+
+.chore-due-date {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
   font-size: 0.875rem;
-  color: #666;
+  font-weight: 700;
+  margin-bottom: 0.5rem;
 }
 
-.neo-chore-description {
-  margin-top: 0.5rem;
+.chore-due-date.overdue {
+  color: #dc3545;
+}
+
+.chore-due-date.today {
+  color: #007bff;
+}
+
+.chore-due-date.tomorrow {
+  color: #28a745;
+}
+
+.chore-due-date.this-week {
+  color: #ffc107;
+}
+
+.chore-due-date.later {
+  color: #6c757d;
+}
+
+.chore-due-date .material-icons {
+  font-size: 1rem;
+}
+
+.chore-description {
   color: #444;
+  line-height: 1.4;
+  font-size: 0.9rem;
 }
 
-.neo-chore-frequency {
-  font-size: 0.875rem;
-  padding: 0.25rem 0.75rem;
-  border-radius: 1rem;
-  font-weight: 600;
+.chore-actions {
+  display: flex;
+  gap: 0.75rem;
 }
 
-.neo-chore-frequency.one_time { background: #e0e0e0; }
-.neo-chore-frequency.daily { background: #bbdefb; color: #1565c0; }
-.neo-chore-frequency.weekly { background: #c8e6c9; color: #2e7d32; }
-.neo-chore-frequency.monthly { background: #e1bee7; color: #7b1fa2; }
-.neo-chore-frequency.custom { background: #ffe0b2; color: #ef6c00; }
+/* Add New Chore Card */
+.timeline-add-card {
+  display: flex;
+  gap: 1rem;
+  padding: 2rem;
+  background: #fff;
+  border: 3px dashed #111;
+  border-radius: 12px;
+  cursor: pointer;
+  transition: all 0.2s ease;
+  align-items: center;
+  justify-content: center;
+}
+
+.timeline-add-card:hover {
+  background: #f8f9fa;
+  transform: translateX(5px);
+}
+
+.add-content {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+  font-weight: 700;
+  color: #111;
+}
+
+.add-content .material-icons {
+  font-size: 2rem;
+}
 
 /* Modal Styles */
-.neo-modal {
+.modal-backdrop {
   position: fixed;
   top: 0;
   left: 0;
@@ -530,7 +1065,7 @@ onMounted(() => {
   z-index: 1000;
 }
 
-.neo-modal-content {
+.modal-container {
   background: white;
   border-radius: 18px;
   box-shadow: 6px 6px 0 #111;
@@ -541,7 +1076,7 @@ onMounted(() => {
   overflow-y: auto;
 }
 
-.neo-modal-header {
+.modal-header {
   padding: 1.5rem;
   border-bottom: 3px solid #111;
   background: #fafafa;
@@ -550,11 +1085,11 @@ onMounted(() => {
   align-items: center;
 }
 
-.neo-modal-body {
+.modal-body {
   padding: 1.5rem;
 }
 
-.neo-modal-footer {
+.modal-footer {
   padding: 1.5rem;
   border-top: 3px solid #111;
   background: #fafafa;
@@ -564,23 +1099,18 @@ onMounted(() => {
 }
 
 /* Form Styles */
-.neo-form {
-  display: flex;
-  flex-direction: column;
-  gap: 1.5rem;
+.form-group {
+  margin-bottom: 1.5rem;
 }
 
-.neo-form-group {
-  display: flex;
-  flex-direction: column;
-  gap: 0.5rem;
-}
-
-.neo-form-group label {
+.form-group label {
+  display: block;
+  margin-bottom: 0.5rem;
   font-weight: 600;
 }
 
-.neo-input {
+.form-input {
+  width: 100%;
   padding: 0.75rem;
   border: 2px solid #111;
   border-radius: 8px;
@@ -588,11 +1118,29 @@ onMounted(() => {
   background: white;
 }
 
-.neo-input:focus {
+.form-input:focus {
   outline: none;
   border-color: var(--primary-color);
 }
 
+.radio-group {
+  display: flex;
+  gap: 1rem;
+}
+
+.radio-group label {
+  display: flex;
+  align-items: center;
+  gap: 0.5rem;
+  font-weight: normal;
+}
+
+.form-text-muted {
+  font-size: 0.875rem;
+  color: #6c757d;
+  margin-top: 0.25rem;
+}
+
 /* Button Styles */
 .btn {
   display: inline-flex;
@@ -604,6 +1152,7 @@ onMounted(() => {
   font-weight: 600;
   cursor: pointer;
   transition: transform 0.1s ease-in-out;
+  text-decoration: none;
 }
 
 .btn:hover {
@@ -630,47 +1179,109 @@ onMounted(() => {
   font-size: 0.875rem;
 }
 
-.btn-icon-only {
-  padding: 0.5rem;
+/* Empty State */
+.empty-state-card {
+  text-align: center;
+  padding: 3rem 2rem;
+  background: var(--light);
+  border-radius: 18px;
+  box-shadow: 6px 6px 0 #111;
+  border: 3px solid #111;
+  margin: 2rem auto;
+  max-width: 500px;
+}
+
+.empty-state-card .icon-lg {
+  width: 64px;
+  height: 64px;
+  margin-bottom: 1rem;
+  color: #666;
+}
+
+.empty-state-card h3 {
+  font-size: 1.5rem;
+  margin-bottom: 0.5rem;
+  color: #333;
+}
+
+.empty-state-card p {
+  color: #666;
+  margin-bottom: 1.5rem;
 }
 
 /* Responsive Adjustments */
 @media (max-width: 768px) {
-  .neo-grid {
-    grid-template-columns: 1fr;
+  .page-padding {
+    padding: 0.5rem;
+    max-width: 100%;
   }
 
-  .neo-modal-content {
-    width: 95%;
+  .page-header {
+    flex-direction: column;
+    gap: 1rem;
+    align-items: stretch;
   }
-}
 
-.chores-group-title {
-  font-size: 1.8rem;
-  font-weight: 600;
-  margin-top: 2rem;
-  margin-bottom: 1rem;
-  color: var(--primary-color);
-  border-bottom: 2px solid var(--primary-color-light);
-  padding-bottom: 0.5rem;
-}
+  .chores-timeline {
+    margin-left: 1rem;
+  }
 
-.radio-group {
-  display: flex;
-  gap: 1rem;
-  margin-bottom: 0.5rem; /* Optional: for spacing */
-}
+  .chores-timeline::before {
+    left: -0.5rem;
+    width: 2px;
+  }
 
-.radio-group label {
-  display: flex;
-  align-items: center;
-  gap: 0.5rem;
-  font-weight: normal; /* Override potential heavier label weight from neo-form-group */
-}
+  .timeline-dot {
+    left: -1.25rem;
+    width: 16px;
+    height: 16px;
+    border-width: 2px;
+    box-shadow: 0 0 0 2px #111;
+  }
 
-.form-text-muted {
-  font-size: 0.875rem;
-  color: #6c757d; /* Bootstrap muted color, adjust as needed */
-  margin-top: 0.25rem;
+  .timeline-section {
+    margin-bottom: 1.5rem;
+  }
+
+  .timeline-header {
+    padding: 1rem;
+  }
+
+  .timeline-title {
+    font-size: 1.1rem;
+  }
+
+  .timeline-items {
+    padding: 1rem;
+  }
+
+  .timeline-chore-card {
+    padding: 0.75rem;
+    margin-bottom: 1rem;
+  }
+
+  .chore-tags {
+    flex-direction: column;
+    gap: 0.25rem;
+  }
+
+  .chore-actions {
+    flex-direction: column;
+    gap: 0.5rem;
+  }
+
+  .btn-sm {
+    width: 100%;
+    justify-content: center;
+  }
+
+  .timeline-add-card {
+    padding: 1.5rem;
+  }
+
+  .add-content {
+    flex-direction: column;
+    text-align: center;
+  }
 }
 </style>
diff --git a/fe/src/pages/GroupDetailPage.vue b/fe/src/pages/GroupDetailPage.vue
index 8bf38b4..934bdb2 100644
--- a/fe/src/pages/GroupDetailPage.vue
+++ b/fe/src/pages/GroupDetailPage.vue
@@ -116,6 +116,42 @@
         </div>
       </div>
 
+      <!-- Expenses Section -->
+      <div class="mt-4">
+        <div class="neo-card">
+          <div class="neo-card-header">
+            <h3>Group Expenses</h3>
+            <router-link :to="`/groups/${groupId}/expenses`" class="btn btn-primary">
+              <span class="material-icons">payments</span>
+              Manage Expenses
+            </router-link>
+          </div>
+          <div class="neo-card-body">
+            <div v-if="recentExpenses.length > 0" class="neo-expenses-list">
+              <div v-for="expense in recentExpenses" :key="expense.id" class="neo-expense-item">
+                <div class="neo-expense-info">
+                  <span class="neo-expense-name">{{ expense.description }}</span>
+                  <span class="neo-expense-date">{{ formatDate(expense.expense_date) }}</span>
+                </div>
+                <div class="neo-expense-details">
+                  <span class="neo-expense-amount">{{ expense.currency }} {{ formatAmount(expense.total_amount)
+                  }}</span>
+                  <span class="neo-chip" :class="getSplitTypeColor(expense.split_type)">
+                    {{ formatSplitType(expense.split_type) }}
+                  </span>
+                </div>
+              </div>
+            </div>
+            <div v-else class="neo-empty-state">
+              <svg class="icon icon-lg" aria-hidden="true">
+                <use xlink:href="#icon-payments" />
+              </svg>
+              <p>No expenses recorded. Click "Manage Expenses" to add some!</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
     </div>
 
     <div v-else class="alert alert-info" role="status">
@@ -134,6 +170,7 @@ import { useNotificationStore } from '@/stores/notifications';
 import { choreService } from '../services/choreService'
 import type { Chore, ChoreFrequency } from '../types/chore'
 import { format } from 'date-fns'
+import type { Expense } from '@/types/expense'
 
 interface Group {
   id: string | number;
@@ -174,6 +211,9 @@ const { copy, copied, isSupported: clipboardIsSupported } = useClipboard({
 // Chores state
 const upcomingChores = ref<Chore[]>([])
 
+// Add new state for expenses
+const recentExpenses = ref<Expense[]>([])
+
 const fetchActiveInviteCode = async () => {
   if (!groupId.value) return;
   // Consider adding a loading state for this fetch if needed, e.g., initialInviteCodeLoading
@@ -326,9 +366,42 @@ const getFrequencyColor = (frequency: ChoreFrequency) => {
   return colors[frequency]
 }
 
+// Add new methods for expenses
+const loadRecentExpenses = async () => {
+  if (!groupId.value) return
+  try {
+    const response = await apiClient.get(`/api/groups/${groupId.value}/expenses`)
+    recentExpenses.value = response.data.slice(0, 5) // Get only the 5 most recent expenses
+  } catch (error) {
+    console.error('Error loading recent expenses:', error)
+  }
+}
+
+const formatAmount = (amount: string) => {
+  return parseFloat(amount).toFixed(2)
+}
+
+const formatSplitType = (type: string) => {
+  return type.split('_').map(word =>
+    word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
+  ).join(' ')
+}
+
+const getSplitTypeColor = (type: string) => {
+  const colors: Record<string, string> = {
+    equal: 'blue',
+    exact_amounts: 'green',
+    percentage: 'purple',
+    shares: 'orange',
+    item_based: 'teal'
+  }
+  return colors[type] || 'grey'
+}
+
 onMounted(() => {
   fetchGroupDetails();
   loadUpcomingChores();
+  loadRecentExpenses();
 });
 </script>
 
@@ -564,4 +637,91 @@ onMounted(() => {
   font-size: 0.875rem;
   color: #666;
 }
+
+/* Expenses List Styles */
+.neo-expenses-list {
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+}
+
+.neo-expense-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 1rem;
+  border-radius: 12px;
+  background: #fafafa;
+  border: 2px solid #111;
+  transition: transform 0.1s ease-in-out;
+}
+
+.neo-expense-item:hover {
+  transform: translateY(-2px);
+}
+
+.neo-expense-info {
+  display: flex;
+  flex-direction: column;
+  gap: 0.5rem;
+}
+
+.neo-expense-name {
+  font-weight: 600;
+  font-size: 1.1rem;
+}
+
+.neo-expense-date {
+  font-size: 0.875rem;
+  color: #666;
+}
+
+.neo-expense-details {
+  display: flex;
+  align-items: center;
+  gap: 1rem;
+}
+
+.neo-expense-amount {
+  font-weight: 600;
+  font-size: 1.1rem;
+}
+
+.neo-chip {
+  padding: 0.25rem 0.75rem;
+  border-radius: 1rem;
+  font-size: 0.875rem;
+  font-weight: 600;
+  background: #e0e0e0;
+}
+
+.neo-chip.blue {
+  background: #e3f2fd;
+  color: #1976d2;
+}
+
+.neo-chip.green {
+  background: #e8f5e9;
+  color: #2e7d32;
+}
+
+.neo-chip.purple {
+  background: #f3e5f5;
+  color: #7b1fa2;
+}
+
+.neo-chip.orange {
+  background: #fff3e0;
+  color: #f57c00;
+}
+
+.neo-chip.teal {
+  background: #e0f2f1;
+  color: #00796b;
+}
+
+.neo-chip.grey {
+  background: #f5f5f5;
+  color: #616161;
+}
 </style>