C:\ROTA\
├── src\
│ ├── StaffRota.Web\ # Backend .NET API
│ ├── StaffRota.Core\ # Core business logic
│ └── StaffRota.Data\ # Data access layer
├── frontend\
│ └── staff-rota-ui\ # React TypeScript frontend
├── create_shift_assignments_table.sql
├── StaffRota.sln
└── rules.md # This file
- DO NOT use
&&(bash syntax) - it doesn't work in PowerShell - USE
;for command chaining in PowerShell - Example:
cd C:\ROTA\src\StaffRota.Web; dotnet build
- Always use full absolute paths to avoid navigation errors
- Backend:
C:\ROTA\src\StaffRota.Web - Frontend:
C:\ROTA\frontend\staff-rota-ui
# Navigate to backend
cd C:\ROTA\src\StaffRota.Web
# Build project
dotnet build
# Run backend server (background)
dotnet run
# Stop running process
# Find process: tasklist | findstr "StaffRota"
# Kill process: Stop-Process -Id [PID] -Force# Navigate to frontend
cd C:\ROTA\frontend\staff-rota-ui
# Install dependencies
npm install
# Start development server
npm start
# Build for production
npm run build# Run SQL scripts from project root
cd C:\ROTA
# Execute SQL file through backend connection# Terminal 1 - Backend
cd C:\ROTA\src\StaffRota.Web; dotnet run
# Terminal 2 - Frontend
cd C:\ROTA\frontend\staff-rota-ui; npm start- Backend changes: Stop backend → Make changes →
dotnet build→dotnet run - Frontend changes: Hot reload automatically applies changes
- Database changes: Run SQL scripts → Restart backend
- File locked errors: Kill running processes first
- Missing dependencies: Run
dotnet restoreornpm install - Port conflicts: Check if services are already running
- Main service:
C:\ROTA\src\StaffRota.Web\Services\ShiftService.cs - Controller:
C:\ROTA\src\StaffRota.Web\Controllers\ShiftsController.cs - Startup:
C:\ROTA\src\StaffRota.Web\Program.cs - Entities:
C:\ROTA\src\StaffRota.Core\Entities\
- Main app:
C:\ROTA\frontend\staff-rota-ui\src\App.tsx - Shift management:
C:\ROTA\frontend\staff-rota-ui\src\components\ShiftManagement.tsx - Staff assignment dialog:
C:\ROTA\frontend\staff-rota-ui\src\components\StaffAssignmentDialog.tsx
- Entity:
ShiftAssignment(ShiftId, UserId, AssignedAt) - Repository:
ShiftAssignmentRepositoryfor CRUD operations - Service:
ShiftService.AssignUsersToShiftAsync()with validation - API:
/api/shifts/{id}/assignmentsendpoints
- Component:
StaffAssignmentDialogwith dual-list interface - State: Available staff (left) ↔ Assigned staff (right)
- Validation: Prevent over-assignment beyond RequiredStaff limit
- UI: Material-UI with responsive design (90vh height)
- Multiple terminals may be needed (backend + frontend)
- Use
Ctrl+Cto stop running processes - Background processes: Use
isBackground: truein terminal commands
- Use backslashes
\for Windows paths - Wrap paths with spaces in quotes
- Prefer absolute paths over relative navigation
- Build failures: Usually due to running processes locking DLLs
- Port conflicts: Backend (5000/5001) or Frontend (3000)
- Missing files: Use absolute paths, verify file existence
- SQL errors: Ensure database connection and table existence
- Always check if processes are running before building
- Use full directory paths in commands
- Verify file changes before executing operations
- Test both frontend and backend after changes
- Keep terminal output for debugging reference
Based on learnings from implementing the Multi-Staff Assignment System
When adding complex new features, changes must be coordinated across all layers of the stack. Missing any layer will result in incomplete functionality.
Location: C:\ROTA\create_[feature]_table.sql
Purpose: Data persistence and relationships
Key Actions:
- Create new tables with proper foreign keys
- Add indexes for performance
- Add unique constraints to prevent duplicates
- Migrate existing data if needed
- Critical: Ensure table is actually created in database file
Example - Multi-Staff Assignments:
CREATE TABLE "ShiftAssignments" (
"Id" INTEGER PRIMARY KEY AUTOINCREMENT,
"ShiftId" INTEGER NOT NULL,
"UserId" INTEGER NOT NULL,
CONSTRAINT "FK_ShiftAssignments_Shifts_ShiftId"
FOREIGN KEY ("ShiftId") REFERENCES "Shifts" ("Id") ON DELETE CASCADE
);Location: C:\ROTA\src\StaffRota.Core\Entities\
Purpose: Define data models and business rules
Key Actions:
- Create new entity classes
- Add navigation properties for relationships
- Define enums for status/type fields
- Add validation attributes
Example:
public class ShiftAssignment
{
public int Id { get; set; }
public int ShiftId { get; set; }
public int UserId { get; set; }
public virtual Shift Shift { get; set; }
public virtual User User { get; set; }
}Location: C:\ROTA\src\StaffRota.Data\Repositories\
Purpose: Database operations and query logic
Key Actions:
- Create repository interface (
I[Feature]Repository) - Implement repository class with CRUD operations
- Add Entity Framework configuration
- Register in dependency injection
Critical Methods:
GetByIdAsync()- Single record retrievalGetByParentIdAsync()- Related records (e.g., assignments by shift)CreateAsync()/UpdateAsync()/DeleteAsync()- CRUDHasConflictAsync()- Business rule validation
Location: C:\ROTA\src\StaffRota.Web\Services\
Purpose: Business rules, validation, and orchestration
Key Actions:
- Add methods to existing services OR create new service
- Implement business validation (staffing limits, conflicts, etc.)
- Handle transaction logic
- Return structured results (success/failure with details)
Critical Pattern - Replace vs Add Logic:
// WRONG: Only adds new records
public async Task AssignUsersAsync(int shiftId, List<int> userIds)
{
foreach(var userId in userIds) {
await _repository.CreateAsync(new Assignment { ShiftId = shiftId, UserId = userId });
}
}
// RIGHT: Replaces complete assignment list
public async Task AssignUsersAsync(int shiftId, List<int> userIds)
{
// Remove existing
var existing = await _repository.GetByShiftIdAsync(shiftId);
foreach(var assignment in existing) {
await _repository.DeleteAsync(assignment.Id);
}
// Add new
foreach(var userId in userIds) {
await _repository.CreateAsync(new Assignment { ShiftId = shiftId, UserId = userId });
}
}Location: C:\ROTA\src\StaffRota.Web\Controllers\
Purpose: HTTP endpoints and request/response handling
Key Actions:
- Add new endpoints OR extend existing controllers
- Add request/response DTOs
- Implement proper HTTP verbs (GET, POST, PUT, DELETE)
- Add authorization attributes
- Critical: Use async mapping methods that load related data
Data Loading Pattern:
// WRONG: Uses static mapping without related data
var shiftDtos = shifts.Select(MapToShiftDto);
// RIGHT: Uses async mapping with related data loading
var shiftDtos = new List<ShiftDto>();
foreach (var shift in shifts) {
var shiftDto = await MapToShiftDtoAsync(shift); // Loads assignments
shiftDtos.Add(shiftDto);
}Location: C:\ROTA\frontend\staff-rota-ui\src\services\
Purpose: API communication and data transformation
Key Actions:
- Add methods to existing service OR create new service
- Handle request/response format differences
- Add proper error handling
- Critical: Match backend property naming (PascalCase vs camelCase)
Naming Convention Fix:
// Frontend sends
const requestData = {
userIds: [1, 2, 3],
notes: "example"
};
// Backend expects (fix in service)
const requestData = {
UserIds: assignmentData.userIds, // Convert case
Notes: assignmentData.notes || '',
ShiftId: id,
SendNotification: true
};Location: C:\ROTA\frontend\staff-rota-ui\src\components\
Purpose: User interface and user experience
Key Actions:
- Create new components OR extend existing ones
- Implement proper state management
- Add form validation and error handling
- Critical: Call refresh/reload after data changes
UI Refresh Pattern:
const handleSave = async () => {
await ServiceClass.saveData(data);
// CRITICAL: Refresh data to show changes
await loadData();
onClose(); // Close dialog after refresh
};Location: C:\ROTA\src\StaffRota.Web\Program.cs
Purpose: Wire up all components
Key Actions:
- Register new repositories in DI container
- Register new services in DI container
- Ensure proper lifetime management (Scoped for Entity Framework)
Example:
builder.Services.AddScoped<IShiftAssignmentRepository, ShiftAssignmentRepository>();When adding new functionality, verify each layer:
□ Database Layer
- SQL script created and tested
- Tables/indexes actually exist in database file
- Foreign key relationships established
- Migration of existing data (if needed)
□ Entity Layer
- Entity classes created with proper properties
- Navigation properties for relationships
- Enums defined for status fields
□ Repository Layer
- Interface created with all needed methods
- Implementation with proper async/await
- Entity Framework configuration
- Dependency injection registration
□ Service Layer
- Business logic methods implemented
- Validation rules enforced
- Transaction handling
- Error handling with structured results
□ API Layer
- Endpoints created with proper HTTP verbs
- Request/response DTOs defined
- Authorization attributes added
- ASYNC mapping methods for related data
□ Frontend Service Layer
- API client methods created
- Request/response transformation
- Property name case conversion (camelCase ↔ PascalCase)
- Error handling
□ Frontend Component Layer
- UI components created/updated
- State management implemented
- Form validation added
- Data refresh after mutations
□ Integration Layer
- Dependency injection configured
- Services registered with proper lifetimes
1. "API returns 200 but no data changes"
- Cause: Using static mapping methods that don't load related data
- Solution: Use async mapping methods with data loading
2. "Frontend shows old data after save"
- Cause: Missing data refresh after mutations
- Solution: Call
loadData()after save operations
3. "Assignment adds instead of replaces"
- Cause: Business logic only handles additions
- Solution: Implement "clear existing + add new" pattern
4. "Missing table errors"
- Cause: SQL script not properly executed
- Solution: Verify table exists in actual database file
5. "Case sensitivity API errors"
- Cause: Frontend camelCase vs Backend PascalCase
- Solution: Transform property names in frontend service layer
6. "Date range filtering excludes records"
- Cause: Default API filters too restrictive
- Solution: Use wide date ranges or remove default filters
For each new feature, test the complete flow:
- Create new record → Verify in database
- Read records → Verify UI displays correctly
- Update existing record → Verify changes persist
- Delete record → Verify removal
- Validation → Test business rules and limits
- Edge cases → Empty states, conflicts, errors
When functionality doesn't work:
- Check database - Is data actually saved?
- Check API responses - Use browser dev tools network tab
- Check property names - Frontend vs backend naming
- Check data loading - Are related records included?
- Check refresh logic - Does UI update after changes?
This pattern applies to any complex feature: recurring shifts, department management, notifications, etc.