using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using JobTrackerApi.Data; using JobTrackerApi.Models; using System.Security.Claims; namespace JobTrackerApi.Controllers { [ApiController] [Route("api/companies")] public class CompaniesController : ControllerBase { private readonly JobTrackerContext _db; public CompaniesController(JobTrackerContext db) { _db = db; } private string? CurrentUserId => User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? User?.FindFirstValue("sub"); private static string? NormalizeSource(string? source) { if (string.IsNullOrWhiteSpace(source)) return null; var value = source.Trim(); if (Uri.TryCreate(value, UriKind.Absolute, out var uri) && !string.IsNullOrWhiteSpace(uri.Host)) { value = uri.Host; } value = value.Replace("www.", "", StringComparison.OrdinalIgnoreCase).Trim().Trim('/'); var lower = value.ToLowerInvariant(); return lower switch { "linkedin" or "linkedin.com" => "LinkedIn", "finn" or "finn.no" => "Finn", "nav" or "nav.no" => "NAV", "jobbnorge" or "jobbnorge.no" => "Jobbnorge", _ => string.Join(" ", value.Split(new[] { ' ', '-', '_' }, StringSplitOptions.RemoveEmptyEntries).Select(part => char.ToUpperInvariant(part[0]) + part[1..].ToLowerInvariant())) }; } [HttpGet] public async Task>> GetAll(CancellationToken cancellationToken) { var userId = CurrentUserId; var q = _db.Companies.AsQueryable(); if (!string.IsNullOrWhiteSpace(userId)) q = q.Where(c => c.OwnerUserId == userId); var companies = await q .OrderBy(c => c.Name) .ToListAsync(cancellationToken); return Ok(companies); } [HttpGet("{id:int}")] public async Task> GetById([FromRoute] int id, CancellationToken cancellationToken) { var userId = CurrentUserId; var q = _db.Companies.AsQueryable(); if (!string.IsNullOrWhiteSpace(userId)) q = q.Where(c => c.OwnerUserId == userId); var company = await q.FirstOrDefaultAsync(c => c.Id == id, cancellationToken); if (company is null) return NotFound(); return Ok(company); } public sealed record CreateCompanyRequest(string Name, string? Location, string? Source); public sealed record UpdateCompanyRequest( string Name, string? Location, string? Source, string? RecruiterName, string? RecruiterEmail, string? RecruiterLinkedIn, DateTime? LastContactedAt, DateTime? NextContactAt, string? PipelineStage ); [HttpPost] public async Task> Create([FromBody] CreateCompanyRequest request, CancellationToken cancellationToken) { var userId = CurrentUserId; var name = (request.Name ?? "").Trim(); if (name.Length == 0) return BadRequest("Company name is required."); var existingQuery = _db.Companies.AsQueryable(); if (!string.IsNullOrWhiteSpace(userId)) existingQuery = existingQuery.Where(c => c.OwnerUserId == userId); var existing = await existingQuery .FirstOrDefaultAsync(c => c.Name.ToLower() == name.ToLower(), cancellationToken); if (existing is not null) { // Idempotent create: return existing instead of failing. return Ok(existing); } var company = new Company { OwnerUserId = string.IsNullOrWhiteSpace(userId) ? null : userId, Name = name, Location = string.IsNullOrWhiteSpace(request.Location) ? null : request.Location.Trim(), Source = NormalizeSource(request.Source), }; _db.Companies.Add(company); await _db.SaveChangesAsync(cancellationToken); return CreatedAtAction(nameof(GetById), new { id = company.Id }, company); } [HttpPut("{id:int}")] public async Task> Update([FromRoute] int id, [FromBody] UpdateCompanyRequest request, CancellationToken cancellationToken) { var userId = CurrentUserId; var q = _db.Companies.AsQueryable(); if (!string.IsNullOrWhiteSpace(userId)) q = q.Where(c => c.OwnerUserId == userId); var company = await q.FirstOrDefaultAsync(c => c.Id == id, cancellationToken); if (company is null) return NotFound(); var name = (request.Name ?? "").Trim(); if (name.Length == 0) return BadRequest("Company name is required."); company.Name = name; company.Location = string.IsNullOrWhiteSpace(request.Location) ? null : request.Location.Trim(); company.Source = NormalizeSource(request.Source); company.RecruiterName = string.IsNullOrWhiteSpace(request.RecruiterName) ? null : request.RecruiterName.Trim(); company.RecruiterEmail = string.IsNullOrWhiteSpace(request.RecruiterEmail) ? null : request.RecruiterEmail.Trim(); company.RecruiterLinkedIn = string.IsNullOrWhiteSpace(request.RecruiterLinkedIn) ? null : request.RecruiterLinkedIn.Trim(); company.PipelineStage = string.IsNullOrWhiteSpace(request.PipelineStage) ? null : request.PipelineStage.Trim(); company.LastContactedAt = request.LastContactedAt; company.NextContactAt = request.NextContactAt; await _db.SaveChangesAsync(cancellationToken); return Ok(company); } } }