feat: Add shared components, refactor stores, improve data-entry workflow

Shared Components:
- Add CompanySelector.vue and PeriodSelector.vue components
- Add AppHeader.vue and SlideMenu.vue layout components
- Add shared stores factories (companies.js, accountingPeriod.js)
- Add shared routes factories (companies.py, calendar.py)
- Add shared models (company.py, calendar.py)
- Add shared layout styles (header.css, navigation.css)

Data Entry App:
- Update CLAUDE.md with prod/test server documentation
- Improve nomenclature sync service with better error handling
- Update receipts router and CRUD operations
- Add company/period stores using shared factories
- Update App.vue layout with shared components
- Fix OCRUploadZone file handling

Reports App:
- Refactor stores to use shared factories
- Update App.vue to use shared layout components

Infrastructure:
- Replace start-data-entry.sh with separate dev/test scripts
- Add .claude/rules for authentication, backend patterns, etc.
- Add implementation plan for OCR receipt improvements
- Clean up old documentation files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-15 15:00:45 +02:00
parent c5fde510a8
commit 1a6e9b17d2
47 changed files with 4079 additions and 2595 deletions

View File

@@ -1,9 +1,9 @@
"""API endpoints for receipts."""
from typing import List, Optional
from typing import List, Optional, Annotated
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Query, Header
from fastapi.responses import FileResponse
from sqlalchemy.ext.asyncio import AsyncSession
@@ -39,20 +39,69 @@ from auth.models import CurrentUser
router = APIRouter()
# ============ Helper for current user's company ============
# ============ Helper for selected company from header ============
async def get_selected_company(
current_user: CurrentUser = Depends(get_current_user),
x_selected_company: Annotated[Optional[str], Header()] = None
) -> int:
"""
Get selected company from X-Selected-Company header.
Validates that the user has access to the specified company.
Falls back to user's first company if no header is provided.
Raises:
HTTPException 403: If user doesn't have access to specified company
HTTPException 400: If user has no companies assigned
"""
if x_selected_company:
try:
company_id = int(x_selected_company)
except ValueError:
raise HTTPException(
status_code=400,
detail=f"Invalid company ID format: {x_selected_company}"
)
# Validate user has access to this company
# Auth stores companies as strings
if str(company_id) in current_user.companies:
return company_id
raise HTTPException(
status_code=403,
detail=f"Nu aveți acces la firma {company_id}"
)
# No header - use first company from user's list
if current_user.companies:
try:
return int(current_user.companies[0])
except (ValueError, IndexError):
pass
raise HTTPException(
status_code=400,
detail="Nu aveți nicio firmă asignată"
)
# Dependency for injection
SelectedCompany = Annotated[int, Depends(get_selected_company)]
# Legacy function for backwards compatibility (deprecated)
def get_current_user_company(current_user: CurrentUser) -> int:
"""
Get current user's active company.
Returns the first company from the user's companies list.
In future, this can be enhanced to use a session-based active company.
DEPRECATED: Use get_selected_company() dependency instead.
This function returns the first company, ignoring X-Selected-Company header.
"""
if current_user.companies:
# For data-entry-app, we assume company ID is numeric
# If companies are stored as strings, convert to int
# For now, return 1 as default (Phase 1)
return 1
try:
return int(current_user.companies[0])
except (ValueError, IndexError):
return 1
return 1
@@ -80,16 +129,14 @@ async def list_receipts(
page: int = Query(default=1, ge=1),
page_size: int = Query(default=20, ge=1, le=100),
session: AsyncSession = Depends(get_session),
current_user: CurrentUser = Depends(get_current_user),
selected_company: SelectedCompany = None,
):
"""Get paginated list of receipts with filters."""
from datetime import date as date_type
current_company = get_current_user_company(current_user)
filters = ReceiptFilter(
status=status,
company_id=company_id or current_company,
company_id=company_id or selected_company,
created_by=created_by,
date_from=date_type.fromisoformat(date_from) if date_from else None,
date_to=date_type.fromisoformat(date_to) if date_to else None,
@@ -105,12 +152,11 @@ async def list_receipts(
async def list_pending_receipts(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
current_user: CurrentUser = Depends(get_current_user),
selected_company: SelectedCompany = None,
):
"""Get all receipts pending review (for accountant view)."""
current_company = get_current_user_company(current_user)
receipts = await ReceiptCRUD.get_pending_review(
session, company_id or current_company
session, company_id or selected_company
)
return [ReceiptResponse.model_validate(r) for r in receipts]
@@ -120,13 +166,13 @@ async def get_receipt_stats(
company_id: Optional[int] = None,
my_receipts: bool = False,
session: AsyncSession = Depends(get_session),
selected_company: SelectedCompany = None,
current_user: CurrentUser = Depends(get_current_user),
):
"""Get receipt statistics."""
current_company = get_current_user_company(current_user)
return await ReceiptCRUD.get_stats(
session,
company_id or current_company,
company_id or selected_company,
created_by=current_user.username if my_receipts else None,
)
@@ -415,12 +461,11 @@ async def get_partners(
search: Optional[str] = None,
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
current_user: CurrentUser = Depends(get_current_user),
selected_company: SelectedCompany = None,
):
"""Get partners (suppliers/customers) for dropdown."""
current_company = get_current_user_company(current_user)
return await NomenclatureService.get_partners(
company_id or current_company, search, session
company_id or selected_company, search, session
)
@@ -428,12 +473,11 @@ async def get_partners(
async def get_accounts(
prefix: Optional[str] = None,
company_id: Optional[int] = None,
current_user: CurrentUser = Depends(get_current_user),
selected_company: SelectedCompany = None,
):
"""Get chart of accounts for dropdown."""
current_company = get_current_user_company(current_user)
return await NomenclatureService.get_accounts(
company_id or current_company, prefix
company_id or selected_company, prefix
)
@@ -441,11 +485,10 @@ async def get_accounts(
async def get_cash_registers(
company_id: Optional[int] = None,
session: AsyncSession = Depends(get_session),
current_user: CurrentUser = Depends(get_current_user),
selected_company: SelectedCompany = None,
):
"""Get cash registers and bank accounts for dropdown."""
current_company = get_current_user_company(current_user)
return await NomenclatureService.get_cash_registers(company_id or current_company, session)
return await NomenclatureService.get_cash_registers(company_id or selected_company, session)
@router.get("/nomenclature/expense-types", response_model=List[ExpenseTypeOption])