Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ──────────────────────────────────────────────
# Docker
# ──────────────────────────────────────────────

# Local override compose files
docker-compose.override.yml

# ──────────────────────────────────────────────
# Node / Frontend
# ──────────────────────────────────────────────

**/node_modules/
**/dist/
**/build/
.next/
.nuxt/
.output/

# ──────────────────────────────────────────────
# OS / Editor noise
# ──────────────────────────────────────────────

.DS_Store
Thumbs.db
*.swp
*.swo
*~

# VS Code workspace settings (keep launch/tasks, ignore local overrides)
.vscode/settings.json
.vscode/*.code-workspace
36 changes: 36 additions & 0 deletions BotNetApi/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# ──────────────────────────────────────────────
# .NET / ASP.NET Core — BotNetApi
# ──────────────────────────────────────────────

# Build output
bin/
obj/

# Published output
publish/
out/

# NuGet packages (restored automatically, never committed)
*.nupkg
*.snupkg
packages/
!packages/build/

# User-specific Visual Studio files
*.user
*.suo
*.userosscache
*.sln.docstates
.vs/

# Rider IDE
.idea/

# ──────────────────────────────────────────────
# Local environment / secrets
# ──────────────────────────────────────────────

# Keep production appsettings, ignore local overrides with real connection strings
appsettings.Development.json
secrets.json
UserSecrets/
22 changes: 22 additions & 0 deletions BotNetApi/BotNetApi.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.8">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
</ItemGroup>

</Project>
102 changes: 102 additions & 0 deletions BotNetApi/Controllers/BotsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
using BotNetApi.DTOs;
using BotNetApi.Services;
using Microsoft.AspNetCore.Mvc;

namespace BotNetApi.Controllers;

[ApiController]
[Route("api/bots")]
public class BotsController : ControllerBase
{
private readonly IBotService _botService;

public BotsController(IBotService botService)
{
_botService = botService;
}

// GET /api/bots
[HttpGet]
public async Task<IActionResult> GetAll()
{
var bots = await _botService.GetAllAsync();
return Ok(bots);
}

// GET /api/bots/findNearest?latitude=47.66&longitude=-117.43
// Declared before {id:int} — prevents any potential route ambiguity
[HttpGet("findNearest")]
public async Task<IActionResult> FindNearest([FromQuery] double latitude, [FromQuery] double longitude)
{
var bot = await _botService.FindNearestAvailableAsync(latitude, longitude);

if (bot is null)
return NotFound(new { message = "No available bots found near the specified location." });

return Ok(bot);
}

// GET /api/bots/{id}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var bot = await _botService.GetByIdAsync(id);
return bot is null ? NotFound() : Ok(bot);
}

// POST /api/bots
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateBotDto dto)
{
var created = await _botService.CreateAsync(dto);
return CreatedAtAction(nameof(GetById), new { id = created.Id }, created);
}

// PUT /api/bots/{id}
[HttpPut("{id:int}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateBotDto dto)
{
var updated = await _botService.UpdateAsync(id, dto);
return updated is null ? NotFound() : Ok(updated);
}

// DELETE /api/bots/{id}
[HttpDelete("{id:int}")]
public async Task<IActionResult> Delete(int id)
{
var deleted = await _botService.DeleteAsync(id);
return deleted ? NoContent() : NotFound();
}

// PUT /api/bots/{id}/recharge
[HttpPut("{id:int}/recharge")]
public async Task<IActionResult> Recharge(int id)
{
var updated = await _botService.RechargeAsync(id);
return updated is null ? NotFound() : Ok(updated);
}

// PUT /api/bots/{id}/stock
[HttpPut("{id:int}/stock")]
public async Task<IActionResult> UpdateStock(int id, [FromBody] UpdateStockDto dto)
{
var updated = await _botService.UpdateStockAsync(id, dto);
return updated is null ? NotFound() : Ok(updated);
}

// PUT /api/bots/{id}/location
[HttpPut("{id:int}/location")]
public async Task<IActionResult> UpdateLocation(int id, [FromBody] UpdateLocationDto dto)
{
var updated = await _botService.UpdateLocationAsync(id, dto);
return updated is null ? NotFound() : Ok(updated);
}

// PUT /api/bots/{id}/servicing-status
[HttpPut("{id:int}/servicing-status")]
public async Task<IActionResult> UpdateServicingStatus(int id, [FromBody] UpdateServicingStatusDto dto)
{
var updated = await _botService.UpdateServicingStatusAsync(id, dto);
return updated is null ? NotFound() : Ok(updated);
}
}
14 changes: 14 additions & 0 deletions BotNetApi/DTOs/BotResponseDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace BotNetApi.DTOs;

public class BotResponseDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string StockLevel { get; set; } = string.Empty;
public int BatteryLevel { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public DateTime LastUpdated { get; set; }
public bool IsOnline { get; set; }
public bool IsServicingCustomer { get; set; }
}
25 changes: 25 additions & 0 deletions BotNetApi/DTOs/CreateBotDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
using BotNetApi.Models;

namespace BotNetApi.DTOs;

public class CreateBotDto
{
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;

[Required]
public StockLevel StockLevel { get; set; }

[Range(0, 100, ErrorMessage = "BatteryLevel must be between 0 and 100.")]
public int BatteryLevel { get; set; } = 100;

[Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")]
public double Latitude { get; set; }

[Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")]
public double Longitude { get; set; }

public bool IsOnline { get; set; } = true;
}
27 changes: 27 additions & 0 deletions BotNetApi/DTOs/UpdateBotDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.ComponentModel.DataAnnotations;
using BotNetApi.Models;

namespace BotNetApi.DTOs;

public class UpdateBotDto
{
[Required]
[MaxLength(100)]
public string Name { get; set; } = string.Empty;

[Required]
public StockLevel StockLevel { get; set; }

[Range(0, 100, ErrorMessage = "BatteryLevel must be between 0 and 100.")]
public int BatteryLevel { get; set; }

[Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")]
public double Latitude { get; set; }

[Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")]
public double Longitude { get; set; }

public bool IsOnline { get; set; }

public bool IsServicingCustomer { get; set; }
}
12 changes: 12 additions & 0 deletions BotNetApi/DTOs/UpdateLocationDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

namespace BotNetApi.DTOs;

public class UpdateLocationDto
{
[Range(-90.0, 90.0, ErrorMessage = "Latitude must be between -90 and 90.")]
public double Latitude { get; set; }

[Range(-180.0, 180.0, ErrorMessage = "Longitude must be between -180 and 180.")]
public double Longitude { get; set; }
}
6 changes: 6 additions & 0 deletions BotNetApi/DTOs/UpdateServicingStatusDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace BotNetApi.DTOs;

public class UpdateServicingStatusDto
{
public bool IsServicingCustomer { get; set; }
}
10 changes: 10 additions & 0 deletions BotNetApi/DTOs/UpdateStockDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;
using BotNetApi.Models;

namespace BotNetApi.DTOs;

public class UpdateStockDto
{
[Required]
public StockLevel StockLevel { get; set; }
}
75 changes: 75 additions & 0 deletions BotNetApi/Data/AppDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using BotNetApi.Models;
using Microsoft.EntityFrameworkCore;

namespace BotNetApi.Data;

public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

public DbSet<Bot> Bots => Set<Bot>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Bot>(entity =>
{
entity.HasKey(b => b.Id);
entity.Property(b => b.Name).IsRequired().HasMaxLength(100);

// Store enum as a readable string rather than an integer
entity.Property(b => b.StockLevel).HasConversion<string>();

// Seed data — real coordinates around downtown Spokane, WA
entity.HasData(
new Bot
{
Id = 1,
Name = "BOT-ALPHA",
StockLevel = StockLevel.High,
BatteryLevel = 92,
Latitude = 47.6588,
Longitude = -117.4260,
LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
IsOnline = true,
IsServicingCustomer = false
},
new Bot
{
Id = 2,
Name = "BOT-BRAVO",
StockLevel = StockLevel.Medium,
BatteryLevel = 61,
Latitude = 47.6721,
Longitude = -117.3982,
LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
IsOnline = true,
IsServicingCustomer = true // busy — should be skipped in nearest-bot search
},
new Bot
{
Id = 3,
Name = "BOT-CHARLIE",
StockLevel = StockLevel.Low,
BatteryLevel = 8, // critically low — should be skipped
Latitude = 47.6543,
Longitude = -117.4390,
LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
IsOnline = false,
IsServicingCustomer = false
},
new Bot
{
Id = 4,
Name = "BOT-DELTA",
StockLevel = StockLevel.High,
BatteryLevel = 77,
Latitude = 47.6489,
Longitude = -117.4143,
LastUpdated = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc),
IsOnline = true,
IsServicingCustomer = false
}
);
});
}
}
Loading