bunch o stuff

This commit is contained in:
2026-04-25 13:14:08 -05:00
parent fc7db1fedd
commit a1fddb3513
18 changed files with 1037 additions and 190 deletions

View File

@@ -1,86 +1,108 @@
@page "/book" @page "/book"
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Forms @using Microsoft.AspNetCore.Components.Forms
<div class="container booking-page"> @if(!Complete)
<div class="section-title"> {
<h1>Request an Appointment</h1> <div class="container booking-page">
<p>Complete the form below and a technician will review your case.</p> <div class="section-title">
<h1>Request an Appointment</h1>
<p>Complete the form below and a technician will review your case.</p>
</div>
<EditForm FormName="RepairRequestForm" Model="Model" OnValidSubmit="HandleSubmit" On class="booking-form-wrapper">
<DataAnnotationsValidator />
<div class="form-grid">
<div class="form-column">
<h3 class="form-heading">1. Appliance Details</h3>
<div class="field-group">
<label>Appliance Type</label>
<InputSelect @bind-Value="Model.Type" class="custom-input">
<option value="">Select an appliance...</option>
<option>Refrigerator</option>
<option>Washing Machine</option>
<option>Dishwasher</option>
<option>Oven / Stove</option>
<option>Dryer</option>
</InputSelect>
<ValidationMessage For="@(() => Model.Type)" class="text-danger" />
</div>
<div class="field-group">
<label>Appliance Brand</label>
<InputSelect @bind-Value="Model.Brand" class="custom-input">
<option value="">Select a brand...</option>
<option>LG</option>
<option>Maytag</option>
<option>Whirlpool</option>
<option>Kitchen Aid</option>
<option>Amana</option>
<option>GE</option>
<option>Samsung</option>
<option>Bosch</option>
<option>Frigidaire</option>
<option>Kenmore</option>
<option>Other (please include in notes)</option>
</InputSelect>
<ValidationMessage For="@(() => Model.Brand)" class="text-danger" />
</div>
<div class="field-group">
<label>Describe the Issue</label>
<InputTextArea @bind-Value="Model.Notes"
placeholder="e.g. Making a clicking noise, not draining, error code F1E2..."
class="custom-input textarea" />
<ValidationMessage For="@(() => Model.Notes)" class="text-danger" />
</div>
</div>
<div class="form-column">
<h3 class="form-heading">2. Photos & Contact</h3>
<div class="upload-zone">
<p><strong>Upload Photos/Video (10MB max)</strong></p>
<p class="small-text">Tip: A photo of the <u>model number sticker</u> helps us arrive with the right parts!</p>
<InputFile OnChange="HandleFiles" multiple class="file-input" id="file-upload" />
<label for="file-upload" class="btn btn-secondary">Add Media</label>
@if (SelectedFiles.Any())
{
<div class="file-count">@SelectedFiles.Count files attached</div>
}
</div>
<div class="field-group">
<label>Full Name</label>
<InputText @bind-Value="Model.Name" class="custom-input" />
<ValidationMessage For="@(() => Model.Name)" class="text-danger" />
</div>
<div class="field-group">
<label>Phone Number</label>
<InputText @bind-Value="Model.Phone" class="custom-input" />
<ValidationMessage For="@(() => Model.Phone)" class="text-danger" />
</div>
</div>
</div>
<div class="form-footer">
<button type="submit" class="btn btn-primary btn-large">Submit Repair Request</button>
</div>
</EditForm>
</div> </div>
}
<EditForm Model="@Model" OnValidSubmit="HandleSubmit" class="booking-form-wrapper"> else
<DataAnnotationsValidator /> {
<div class="complete-container">
<div class="form-grid"> <div class="complete-content">
<div class="form-column"> <h1 class="complete-heading">Thank You!</h1>
<h3 class="form-heading">1. Appliance Details</h3> <p class="complete-subheading">We will be contacting you shortly.</p>
<p class="complete-subheading">Your request number is: @Model.RequestNumber</p>
<div class="field-group"> <NavLink class="btn-home" href="" Match="NavLinkMatch.All">
<label>Appliance Type</label> <span class="home-icon">🏠</span> Back to Home
<InputSelect @bind-Value="Model.Type" class="custom-input"> </NavLink>
<option value="">Select an appliance...</option>
<option>Refrigerator</option>
<option>Washing Machine</option>
<option>Dishwasher</option>
<option>Oven / Stove</option>
<option>Dryer</option>
</InputSelect>
</div>
<div class="field-group">
<label>Appliance Brand</label>
<InputSelect @bind-Value="Model.Brand" class="custom-input">
<option value="">Select a brand...</option>
<option>LG</option>
<option>Maytag</option>
<option>Whirlpool</option>
<option>Kitchen Aid</option>
<option>Amana</option>
<option>GE</option>
<option>Samsung</option>
<option>Bosch</option>
<option>Frigidaire</option>
<option>Kenmore</option>
<option>Other (please include in notes)</option>
</InputSelect>
</div>
<div class="field-group">
<label>Describe the Issue</label>
<InputTextArea @bind-Value="Model.Notes"
placeholder="e.g. Making a clicking noise, not draining, error code F1E2..."
class="custom-input textarea" />
</div>
</div>
<div class="form-column">
<h3 class="form-heading">2. Photos & Contact</h3>
<div class="upload-zone">
<p><strong>Upload Photos/Video</strong></p>
<p class="small-text">Tip: A photo of the <u>model number sticker</u> helps us arrive with the right parts!</p>
<InputFile OnChange="HandleFiles" multiple class="file-input" id="file-upload" />
<label for="file-upload" class="btn btn-secondary">Add Media</label>
@if (SelectedFiles.Any())
{
<div class="file-count">@SelectedFiles.Count files attached</div>
}
</div>
<div class="field-group">
<label>Full Name</label>
<InputText @bind-Value="Model.Name" class="custom-input" />
</div>
<div class="field-group">
<label>Phone Number</label>
<InputText @bind-Value="Model.Phone" class="custom-input" />
</div>
</div>
</div> </div>
</div>
<div class="form-footer"> }
<button type="submit" class="btn btn-primary btn-large">Submit Repair Request</button>
</div>
</EditForm>
</div>

View File

@@ -2,16 +2,70 @@
namespace ApplianceRepair.Components.Pages namespace ApplianceRepair.Components.Pages
{ {
public partial class Book() public static class RequestNumberGenerator
{ {
private RepairRequestModel Model = new(); private static readonly char[] _chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789".ToCharArray();
private List<IBrowserFile> SelectedFiles = new();
public static string Generate(int length = 6)
{
var result = new char[length];
// Use Random.Shared in .NET 6+ for thread safety
for (int i = 0; i < length; i++)
{
result[i] = _chars[Random.Shared.Next(_chars.Length)];
}
return new string(result);
}
}
public partial class Book(RepairRequestReader repairRequestReader, RepairRequestMediaReader repairRequestMediaReader)
{
public RepairRequestModel Model = new();
public List<IBrowserFile> SelectedFiles = new();
public bool Complete = false;
private void HandleFiles(InputFileChangeEventArgs e) => SelectedFiles.AddRange(e.GetMultipleFiles()); private void HandleFiles(InputFileChangeEventArgs e) => SelectedFiles.AddRange(e.GetMultipleFiles());
private async Task HandleSubmit() private async Task HandleSubmit()
{ {
// Logic to process the request Model.RequestNumber = RequestNumberGenerator.Generate();
Model.CreatedAt = DateTime.Now;
Model.UpdatedAt = DateTime.Now;
await repairRequestReader.AddRecord(Model);
var imageUploadPath = Path.Combine(Environment.CurrentDirectory, "wwwroot", "uploads");
if (!Directory.Exists(imageUploadPath)) Directory.CreateDirectory(imageUploadPath);
foreach (var file in SelectedFiles)
{
try
{
var trustedFileName = Path.GetRandomFileName() + Path.GetExtension(file.Name);
var path = Path.Combine(imageUploadPath, trustedFileName);
using var stream = file.OpenReadStream(maxAllowedSize: 1024 * 1024 * 10);
using var fileStream = new FileStream(path, FileMode.Create);
await stream.CopyToAsync(fileStream);
var mediaRecord = new RepairRequestMediaRecord()
{
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now,
RequestNumber = Model.RequestNumber,
MediaPath = path,
};
await repairRequestMediaReader.AddRecord(mediaRecord);
}
catch (Exception ex)
{
// probably need to show this to the user somehow, something prettier tho
Console.WriteLine($"File: {file.Name} Error: {ex.Message}");
}
}
Complete = true;
} }
} }
} }

View File

@@ -93,7 +93,61 @@
transition: all 0.3s ease; transition: all 0.3s ease;
} }
/* Match your mobile responsiveness */ .complete-container {
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
padding: 0 20px;
}
.complete-content {
max-width: 800px;
}
.complete-heading {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
}
.complete-subheading {
font-size: 1.2rem;
color: #555;
line-height: 1.5;
}
::deep .btn-home {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
background-color: #007bff;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: 600;
transition: background-color 0.2s ease, transform 0.1s ease;
border: none;
cursor: pointer;
}
::deep .btn-home:hover {
background-color: #0056b3;
color: white;
text-decoration: none;
}
::deep .btn-home:active {
transform: scale(0.98);
}
.home-icon {
margin-right: 8px;
}
@media (max-width: 768px) { @media (max-width: 768px) {
.form-grid { .form-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -102,4 +156,12 @@
.booking-form-wrapper { .booking-form-wrapper {
padding: 20px; padding: 20px;
} }
.complete-heading {
font-size: 1.8rem; /* Slightly smaller for small screens */
}
.complete-subheading {
font-size: 1rem;
}
} }

View File

@@ -2,7 +2,7 @@
namespace ApplianceRepair.Components.Pages namespace ApplianceRepair.Components.Pages
{ {
public partial class Home(IMemoryCache cache, HomePageReader homePageReader, ContentCardReader contentCardReader) public partial class Home(IMemoryCache cache, HomePageReader homePageReader, ContentCardReader contentCardReader, BusinessConfigReader businessConfigReader)
{ {
private HomePageModel? Model; private HomePageModel? Model;
@@ -10,7 +10,12 @@ namespace ApplianceRepair.Components.Pages
{ {
if (!cache.TryGetValue(nameof(HomePageModel), out Model)) if (!cache.TryGetValue(nameof(HomePageModel), out Model))
{ {
Model = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent; var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
Model = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
var cacheOptions = new MemoryCacheEntryOptions() var cacheOptions = new MemoryCacheEntryOptions()
.SetAbsoluteExpiration(TimeSpan.FromHours(24)) .SetAbsoluteExpiration(TimeSpan.FromHours(24))

View File

@@ -21,7 +21,7 @@ else
<h1>@Model.HeaderLine1 <br><span>@Model.HeaderLine2</span></h1> <h1>@Model.HeaderLine1 <br><span>@Model.HeaderLine2</span></h1>
<p>@Model.HeaderText</p> <p>@Model.HeaderText</p>
<div class="cta-group"> <div class="cta-group">
<a href="phone:@Model.FormattedPhoneNumber" class="btn btn-primary">Call for Same-Day Service</a> <a href="@Model.PhoneNumberCallLink" class="btn btn-primary">Call for Same-Day Service</a>
<a href="/book" class="btn btn-secondary">Request an Appointment</a> <a href="/book" class="btn btn-secondary">Request an Appointment</a>
</div> </div>
</div> </div>

View File

@@ -21,16 +21,22 @@
<div class="container"> <div class="container">
<header class="admin-header"> <header class="admin-header">
<div class="tab-container"> <div class="tab-container">
<button class="tab-btn @(currentTab == AdminTab.Home ? "active" : "")" <button class="tab-btn @(CurrentTab == AdminTab.Home ? "active" : "")"
@onclick="() => currentTab = AdminTab.Home"> @onclick="() => CurrentTab = AdminTab.Home">
Home Page Home Page
</button> </button>
</div> </div>
<div class="tab-container">
<button class="tab-btn @(CurrentTab == AdminTab.BusinessInfo ? "active" : "")"
@onclick="() => CurrentTab = AdminTab.BusinessInfo">
Business Info
</button>
</div>
</header> </header>
@if (currentTab == AdminTab.Home) @if (CurrentTab == AdminTab.Home)
{ {
<EditForm FormName="HomePageForm" Model="HomePageModel" OnValidSubmit="SaveHomePageModel" On class="admin-form home-page-form"> <EditForm FormName="HomePageForm" Model="HomePageModel" OnValidSubmit="SaveHomePageModel" On class="admin-form">
<DataAnnotationsValidator /> <DataAnnotationsValidator />
<div class="form-section text-center"> <div class="form-section text-center">
@@ -100,9 +106,35 @@
</div> </div>
</EditForm> </EditForm>
} }
else else if (CurrentTab == AdminTab.BusinessInfo)
{ {
<h1>Another page</h1> <EditForm FormName="BusinessInfoForm" Model="BusinessInfo" OnValidSubmit="SaveBusinessInfo" On class="admin-form">
<DataAnnotationsValidator />
<div class="form-section text-center">
<h3><i class="icon">🏠</i> Business Info</h3>
<div class="input-group">
<label>Business Name</label>
<InputText @bind-Value="BusinessInfo.Name" class="form-input" />
</div>
<div class="input-group">
<label>Phone Number</label>
<InputText @bind-Value="BusinessInfo.PhoneNumber" class="form-input" />
</div>
<div class="input-group">
<label>Support Email</label>
<InputText @bind-Value="BusinessInfo.SupportEmail" class="form-input" />
</div>
</div>
<div class="admin-footer">
<button type="submit" class="btn btn-save">Save</button>
<button type="button" class="btn btn-revert" @onclick="RevertBusinessInfo">Revert</button>
</div>
</EditForm>
} }
</div> </div>
</div> </div>

View File

@@ -1,39 +1,63 @@
namespace ApplianceRepair.Components.Pages.admin namespace ApplianceRepair.Components.Pages.admin
{ {
public partial class EditPages(HomePageReader homePageReader, ContentCardReader contentCardReader) public partial class EditPages(HomePageReader homePageReader, ContentCardReader contentCardReader, BusinessConfigReader businessConfigReader)
{ {
public HomePageModel? HomePageModel; public HomePageModel? HomePageModel;
public BusinessInfoModel? BusinessInfo;
private enum AdminTab { Home, About } private enum AdminTab { Home, About, BusinessInfo }
private AdminTab currentTab = AdminTab.Home; private AdminTab CurrentTab = AdminTab.Home;
override protected override async Task OnInitializedAsync()
protected async void OnInitialized()
{ {
HomePageModel = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent; var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
BusinessInfo = new BusinessInfoModel(businessConfig);
HomePageModel = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
} }
private async void RevertHomePageModel() private async void RevertHomePageModel()
{ {
HomePageModel = await homePageReader.ReadLatestRecordWithModel(contentCardReader) ?? Defaults.DefaultHomePageContent; var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
var latestHomeRecord = await homePageReader.ReadLatestRecord() ?? Defaults.DefaultHomePageContent;
var servicesList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Services)) ?? [];
var trustList = await contentCardReader.ReadAllByPageAndGroup(HomePageModel.PageName, nameof(HomePageModel.ContentCardTypes.Trust)) ?? [];
BusinessInfo = new BusinessInfoModel(businessConfig);
HomePageModel = new HomePageModel(latestHomeRecord, businessConfig, servicesList, trustList);
}
private async void RevertBusinessInfo()
{
var businessConfig = await businessConfigReader.ReadLatestRecord() ?? Defaults.DefaultBusinessConfig;
BusinessInfo = new BusinessInfoModel(businessConfig);
} }
private async void SaveHomePageModel() private async void SaveHomePageModel()
{ {
HomePageModel.CreatedAt = DateTime.Now;
HomePageModel.UpdatedAt = DateTime.Now; HomePageModel.UpdatedAt = DateTime.Now;
await homePageReader.UpdateRecord(HomePageModel);
foreach (var card in HomePageModel.ServicesCards) foreach (var card in HomePageModel.ServicesCards)
{ {
card.UpdatedAt = DateTime.Now;
await contentCardReader.UpdateRecord(card); await contentCardReader.UpdateRecord(card);
} }
foreach (var card in HomePageModel.TrustCards) foreach (var card in HomePageModel.TrustCards)
{ {
card.UpdatedAt = DateTime.Now;
await contentCardReader.UpdateRecord(card); await contentCardReader.UpdateRecord(card);
} }
}
await homePageReader.AddRecord(HomePageModel); private async void SaveBusinessInfo()
{
BusinessInfo.UpdatedAt = DateTime.Now;
await businessConfigReader.UpdateRecord(BusinessInfo);
} }
private void AddServiceCard() private void AddServiceCard()
@@ -42,7 +66,7 @@
CreatedAt = DateTime.Now, CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now, UpdatedAt = DateTime.Now,
BelongsToPage = HomePageModel.PageName, BelongsToPage = HomePageModel.PageName,
Group = HomePageModel.ContentCardTypes.Service.ToString(), Group = HomePageModel.ContentCardTypes.Services.ToString(),
Header = "Service Name", Header = "Service Name",
Text = "Short Description" Text = "Short Description"
}); });

39
Data.cs
View File

@@ -1,4 +1,6 @@
namespace ApplianceRepair using System.ComponentModel.DataAnnotations;
namespace ApplianceRepair
{ {
public class HomePageRecord public class HomePageRecord
{ {
@@ -11,15 +13,10 @@
public string? HeaderText { get; set; } public string? HeaderText { get; set; }
public string? HeaderButton1Text { get; set; } public string? CallHeaderText { get; set; }
public string? HeaderButton2Text { get; set; } public string? BookHeaderText { get; set; }
public string? HeaderButton1Link { get; set; }
public string? HeaderButton2Link { get; set; }
public string? SecondaryHeaderText { get; set; } public string? SecondaryHeaderText { get; set; }
public string? CopyrightText { get; set; }
} }
public class ContentCardRecord public class ContentCardRecord
@@ -47,10 +44,36 @@
public class RepairRequestRecord public class RepairRequestRecord
{ {
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? RequestNumber { get; set; }
[Required(ErrorMessage = "Appliance Type is required.")]
public string? Type { get; set; } public string? Type { get; set; }
[Required(ErrorMessage = "Appliance brand is required.")]
public string? Brand { get; set; } public string? Brand { get; set; }
[Required(ErrorMessage = "Description is required.")]
public string? Notes { get; set; } public string? Notes { get; set; }
[Required(ErrorMessage = "Full Name is required.")]
public string? Name { get; set; } public string? Name { get; set; }
[Required(ErrorMessage = "Phone number is required.")]
[Phone(ErrorMessage = "Please enter a valid phone number.")]
public string? Phone { get; set; } public string? Phone { get; set; }
} }
public class RepairRequestMediaRecord
{
public int Id { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public string? RequestNumber { get; set; }
public string? MediaPath { get; set; }
}
} }

View File

@@ -8,5 +8,47 @@ namespace ApplianceRepair
public DbSet<ContentCardRecord> ContentCards { get; set; } public DbSet<ContentCardRecord> ContentCards { get; set; }
public DbSet<BusinessConfigRecord> BusinessConfig { get; set; } public DbSet<BusinessConfigRecord> BusinessConfig { get; set; }
public DbSet<RepairRequestRecord> RepairRequests { get; set; } public DbSet<RepairRequestRecord> RepairRequests { get; set; }
public DbSet<RepairRequestMediaRecord> RepairRequestMedia { get; set; }
// Seed the data
public static async Task Initialize(DatabaseContext context)
{
if (!context.BusinessConfig.Any())
{
var config = Defaults.DefaultBusinessConfig;
config.CreatedAt = DateTime.Now;
config.UpdatedAt = DateTime.Now;
await context.BusinessConfig.AddAsync(config);
await context.SaveChangesAsync();
}
if (!context.HomePage.Any())
{
var home = Defaults.DefaultHomePageContent;
home.CreatedAt = DateTime.Now;
home.UpdatedAt = DateTime.Now;
await context.HomePage.AddAsync(home);
await context.SaveChangesAsync();
if (!context.ContentCards.Any())
{
foreach (var card in Defaults.DefaultHomePageServiceCards)
{
card.CreatedAt = DateTime.Now;
card.UpdatedAt = DateTime.Now;
await context.ContentCards.AddAsync(card);
}
foreach (var card in Defaults.DefaultHomePageTrustCards)
{
card.CreatedAt = DateTime.Now;
card.UpdatedAt = DateTime.Now;
await context.ContentCards.AddAsync(card);
}
await context.SaveChangesAsync();
}
}
}
} }
} }

View File

@@ -2,39 +2,36 @@
{ {
public static class Defaults public static class Defaults
{ {
public static readonly HomePageModel DefaultHomePageContent = new() public static readonly HomePageRecord DefaultHomePageContent = new()
{ {
BusinessName = "Appliance Pro",
FormattedPhoneNumber = "(555) 012-3456",
PhoneNumberCallLink = "tel:5550123456",
HeaderLine1 = "Expert Appliance Repair,", HeaderLine1 = "Expert Appliance Repair,",
HeaderLine2 = "Done Right Today.", HeaderLine2 = "Done Right Today.",
HeaderText = "Fast, affordable repairs for all major brands. Serving the Greater Metro Area.", HeaderText = "Fast, affordable repairs for all major brands. Serving the Greater Metro Area.",
HeaderButton1Text = "Call for Same-Day Service", CallHeaderText = "Call for Same-Day Service",
HeaderButton1Link = "tel:5550123456", BookHeaderText = "Book Online",
HeaderButton2Text = "Book Online",
HeaderButton2Link = "#booking",
SecondaryHeaderText = "What We Fix", SecondaryHeaderText = "What We Fix",
};
ServicesCards = public static readonly List<ContentCardRecord> DefaultHomePageServiceCards = [
[ new() { Header = "Refrigerators", Text = "Cooling issues, leaks, and compressor repairs.", BelongsToPage = "Home", Group = "Services" },
new() { Header = "Refrigerators", Text = "Cooling issues, leaks, and compressor repairs." }, new() { Header = "Washers & Dryers", Text = "Fixing drum issues, drainage, and heating elements.", BelongsToPage = "Home", Group = "Services" },
new() { Header = "Washers & Dryers", Text = "Fixing drum issues, drainage, and heating elements." }, new() { Header = "Ovens & Ranges", Text = "Electrical igniters, gas flow, and temperature control.", BelongsToPage = "Home", Group = "Services" }
new() { Header = "Ovens & Ranges", Text = "Electrical igniters, gas flow, and temperature control." } ];
],
TrustCards = public static readonly List<ContentCardRecord> DefaultHomePageTrustCards = [
[ new() { Header = "90-Day Warranty", Text = "Quality parts and labor guaranteed.", BelongsToPage = "Home", Group = "Trust" },
new() { Header = "90-Day Warranty", Text = "Quality parts and labor guaranteed." }, new() { Header = "Certified Techs", Text = "Licensed, bonded, and background-checked.", BelongsToPage = "Home", Group = "Trust" },
new() { Header = "Certified Techs", Text = "Licensed, bonded, and background-checked." }, new() { Header = "Fair Pricing", Text = "No hidden fees or diagnostic surprises.", BelongsToPage = "Home", Group = "Trust" }
new() { Header = "Fair Pricing", Text = "No hidden fees or diagnostic surprises." } ];
],
CopyrightText = $"© {DateTime.Now.Year} Appliance Pro. All rights reserved." public static readonly BusinessConfigRecord DefaultBusinessConfig = new()
{
Name = "Appliance Pro",
PhoneNumber = "5550123456",
SupportEmail = "appliance@pro.net"
}; };
} }
} }

View File

@@ -0,0 +1,173 @@
// <auto-generated />
using System;
using ApplianceRepair;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ApplianceRepair.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20260424050555_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ApplianceRepair.BusinessConfigRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<string>("SupportEmail")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("BusinessConfig");
});
modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BelongsToPage")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasColumnType("TEXT");
b.Property<string>("Header")
.HasColumnType("TEXT");
b.Property<string>("Text")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ContentCards");
});
modelBuilder.Entity("ApplianceRepair.HomePageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BookHeaderText")
.HasColumnType("TEXT");
b.Property<string>("CallHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine1")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine2")
.HasColumnType("TEXT");
b.Property<string>("HeaderText")
.HasColumnType("TEXT");
b.Property<string>("SecondaryHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("HomePage");
});
modelBuilder.Entity("ApplianceRepair.RepairRequestMediaRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("MediaPath")
.HasColumnType("TEXT");
b.Property<string>("RequestNumber")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RepairRequestMedia");
});
modelBuilder.Entity("ApplianceRepair.RepairRequestRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Brand")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<string>("Phone")
.HasColumnType("TEXT");
b.Property<string>("RequestNumber")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RepairRequests");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,125 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace ApplianceRepair.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BusinessConfig",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: true),
PhoneNumber = table.Column<string>(type: "TEXT", nullable: true),
SupportEmail = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BusinessConfig", x => x.Id);
});
migrationBuilder.CreateTable(
name: "ContentCards",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
BelongsToPage = table.Column<string>(type: "TEXT", nullable: true),
Group = table.Column<string>(type: "TEXT", nullable: true),
Header = table.Column<string>(type: "TEXT", nullable: true),
Text = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ContentCards", x => x.Id);
});
migrationBuilder.CreateTable(
name: "HomePage",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
HeaderLine1 = table.Column<string>(type: "TEXT", nullable: true),
HeaderLine2 = table.Column<string>(type: "TEXT", nullable: true),
HeaderText = table.Column<string>(type: "TEXT", nullable: true),
CallHeaderText = table.Column<string>(type: "TEXT", nullable: true),
BookHeaderText = table.Column<string>(type: "TEXT", nullable: true),
SecondaryHeaderText = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_HomePage", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RepairRequestMedia",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
RequestNumber = table.Column<string>(type: "TEXT", nullable: true),
MediaPath = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RepairRequestMedia", x => x.Id);
});
migrationBuilder.CreateTable(
name: "RepairRequests",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
CreatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
RequestNumber = table.Column<string>(type: "TEXT", nullable: true),
Type = table.Column<string>(type: "TEXT", nullable: true),
Brand = table.Column<string>(type: "TEXT", nullable: true),
Notes = table.Column<string>(type: "TEXT", nullable: true),
Name = table.Column<string>(type: "TEXT", nullable: true),
Phone = table.Column<string>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_RepairRequests", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BusinessConfig");
migrationBuilder.DropTable(
name: "ContentCards");
migrationBuilder.DropTable(
name: "HomePage");
migrationBuilder.DropTable(
name: "RepairRequestMedia");
migrationBuilder.DropTable(
name: "RepairRequests");
}
}
}

View File

@@ -0,0 +1,170 @@
// <auto-generated />
using System;
using ApplianceRepair;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace ApplianceRepair.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "9.0.12");
modelBuilder.Entity("ApplianceRepair.BusinessConfigRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("PhoneNumber")
.HasColumnType("TEXT");
b.Property<string>("SupportEmail")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("BusinessConfig");
});
modelBuilder.Entity("ApplianceRepair.ContentCardRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BelongsToPage")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Group")
.HasColumnType("TEXT");
b.Property<string>("Header")
.HasColumnType("TEXT");
b.Property<string>("Text")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ContentCards");
});
modelBuilder.Entity("ApplianceRepair.HomePageRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("BookHeaderText")
.HasColumnType("TEXT");
b.Property<string>("CallHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine1")
.HasColumnType("TEXT");
b.Property<string>("HeaderLine2")
.HasColumnType("TEXT");
b.Property<string>("HeaderText")
.HasColumnType("TEXT");
b.Property<string>("SecondaryHeaderText")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("HomePage");
});
modelBuilder.Entity("ApplianceRepair.RepairRequestMediaRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("MediaPath")
.HasColumnType("TEXT");
b.Property<string>("RequestNumber")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RepairRequestMedia");
});
modelBuilder.Entity("ApplianceRepair.RepairRequestRecord", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Brand")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAt")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasColumnType("TEXT");
b.Property<string>("Notes")
.HasColumnType("TEXT");
b.Property<string>("Phone")
.HasColumnType("TEXT");
b.Property<string>("RequestNumber")
.HasColumnType("TEXT");
b.Property<string>("Type")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedAt")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("RepairRequests");
});
#pragma warning restore 612, 618
}
}
}

106
Models.cs
View File

@@ -22,17 +22,58 @@
public class HomePageModel : HomePageRecord public class HomePageModel : HomePageRecord
{ {
public static string PageName = "Home"; public static string PageName = "Home";
public enum ContentCardTypes public enum ContentCardTypes
{ {
Service, Services,
Trust, Trust,
} }
public string BusinessName { get; set; } public string BusinessName { get; set; }
public string FormattedPhoneNumber { get; set; }
public string PhoneNumberCallLink { get; set; } public string PhoneNumber { get; set; }
public string FormattedPhoneNumber
{
get
{
if (!string.IsNullOrEmpty(PhoneNumber))
{
return $"({PhoneNumber[0..3]})-{PhoneNumber[3..6]}-{PhoneNumber[6..10]}";
}
return "";
}
}
public string PhoneNumberCallLink
{
get
{
if (!string.IsNullOrEmpty(PhoneNumber))
{
return $"tel:{PhoneNumber}";
}
return "";
}
}
public string CopyrightText
{
get
{
if (!string.IsNullOrEmpty(BusinessName))
{
return $"© {DateTime.Now.Year} {BusinessName}. All rights reserved.";
}
return $"© {DateTime.Now.Year} All rights reserved.";
}
}
public List<ContentCardModel> ServicesCards { get; set; } public List<ContentCardModel> ServicesCards { get; set; }
public List<ContentCardModel> TrustCards { get; set; } public List<ContentCardModel> TrustCards { get; set; }
public HomePageModel() public HomePageModel()
@@ -40,44 +81,57 @@
HeaderLine1 = string.Empty; HeaderLine1 = string.Empty;
HeaderLine2 = string.Empty; HeaderLine2 = string.Empty;
HeaderText = string.Empty; HeaderText = string.Empty;
HeaderButton1Text = string.Empty; CallHeaderText = string.Empty;
HeaderButton1Link = string.Empty; BookHeaderText = string.Empty;
HeaderButton2Text = string.Empty;
HeaderButton2Link = string.Empty;
SecondaryHeaderText = string.Empty; SecondaryHeaderText = string.Empty;
CopyrightText = string.Empty;
BusinessName = "Appliance Pro"; BusinessName = string.Empty;
FormattedPhoneNumber = "(555) 555-5555"; PhoneNumber = string.Empty;
PhoneNumberCallLink = $"tel:{FormattedPhoneNumber}";
ServicesCards = []; ServicesCards = [];
TrustCards = []; TrustCards = [];
} }
public HomePageModel(HomePageRecord record) public HomePageModel(
HomePageRecord homePageRecord,
BusinessConfigRecord businessConfigRecord,
List<ContentCardRecord> serviceCards,
List<ContentCardRecord> trustCards)
{ {
HeaderLine1 = record.HeaderLine1; HeaderLine1 = homePageRecord.HeaderLine1;
HeaderLine2 = record.HeaderLine2; HeaderLine2 = homePageRecord.HeaderLine2;
HeaderText = record.HeaderText; HeaderText = homePageRecord.HeaderText;
HeaderButton1Text = record.HeaderButton1Text; CallHeaderText = homePageRecord.CallHeaderText;
HeaderButton1Link = record.HeaderButton1Link; BookHeaderText = homePageRecord.BookHeaderText;
HeaderButton2Text = record.HeaderButton2Text; SecondaryHeaderText = homePageRecord.SecondaryHeaderText;;
HeaderButton2Link = record.HeaderButton2Link;
SecondaryHeaderText = record.SecondaryHeaderText;
CopyrightText = record.CopyrightText;
BusinessName = "Appliance Pro"; BusinessName = businessConfigRecord.Name ?? "";
FormattedPhoneNumber = "(555) 555-5555"; PhoneNumber = businessConfigRecord.PhoneNumber ?? "";
PhoneNumberCallLink = $"tel:{FormattedPhoneNumber}";
ServicesCards = []; ServicesCards = [];
TrustCards = []; TrustCards = [];
foreach (var card in serviceCards)
{
ServicesCards.Add(new ContentCardModel(card));
}
foreach (var card in trustCards)
{
TrustCards.Add(new ContentCardModel(card));
}
} }
} }
public class RepairRequestModel : RepairRequestRecord public class RepairRequestModel : RepairRequestRecord { }
{
public class BusinessInfoModel : BusinessConfigRecord
{
public BusinessInfoModel(BusinessConfigRecord record)
{
Name = record.Name;
PhoneNumber = record.PhoneNumber;
SupportEmail = record.SupportEmail;
}
} }
} }

View File

@@ -17,20 +17,26 @@ builder.Services.AddLogging();
builder.Services.AddScoped<BusinessConfigReader>(); builder.Services.AddScoped<BusinessConfigReader>();
builder.Services.AddScoped<ContentCardReader>(); builder.Services.AddScoped<ContentCardReader>();
builder.Services.AddScoped<HomePageReader>(); builder.Services.AddScoped<HomePageReader>();
builder.Services.AddScoped<RepairRequestReader>();
builder.Services.AddScoped<RepairRequestMediaReader>();
var app = builder.Build(); var app = builder.Build();
using (var scope = app.Services.CreateScope()) using (var scope = app.Services.CreateScope())
{ {
var services = scope.ServiceProvider; var services = scope.ServiceProvider;
try try
{ {
services.GetRequiredService<DatabaseContext>().Database.EnsureCreated(); var context = services.GetRequiredService<DatabaseContext>();
await context.Database.MigrateAsync();
await DatabaseContext.Initialize(context);
} }
catch (Exception ex) catch (Exception ex)
{ {
var logger = services.GetRequiredService<ILogger<Program>>(); var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB."); logger.LogError(ex, "An error occurred while migrating or seeding the database.");
} }
} }

View File

@@ -10,37 +10,27 @@ namespace ApplianceRepair
return records; return records;
} }
public async Task<HomePageModel?> ReadLatestRecordWithModel(ContentCardReader contentCardReader)
{
var record = await db.HomePage.OrderByDescending(page => page.Id).FirstOrDefaultAsync();
if (record == null)
{
return null;
}
var model = new HomePageModel(record);
var pageName = HomePageModel.PageName;
var services = await contentCardReader.ReadAllByPageAndGroup(pageName, HomePageModel.ContentCardTypes.Service.ToString()) ?? [];
foreach (var card in services)
{
model.ServicesCards.Add(new ContentCardModel(card));
}
var trust = await contentCardReader.ReadAllByPageAndGroup(pageName, HomePageModel.ContentCardTypes.Trust.ToString()) ?? [];
foreach (var card in trust)
{
model.TrustCards.Add(new ContentCardModel(card));
}
return model;
}
public async Task AddRecord(HomePageRecord record) public async Task AddRecord(HomePageRecord record)
{ {
record.CreatedAt = DateTime.Now;
record.UpdatedAt = DateTime.Now;
await db.AddAsync(record); await db.AddAsync(record);
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
public async Task UpdateRecord(HomePageRecord record)
{
var found = db.HomePage.Where((page) => page.Id == record.Id).FirstOrDefault();
if (found == null)
{
await AddRecord(record);
}
else
{
db.HomePage.Update(record);
}
await db.SaveChangesAsync();
}
} }
public class ContentCardReader(DatabaseContext db) public class ContentCardReader(DatabaseContext db)
@@ -58,6 +48,7 @@ namespace ApplianceRepair
public async Task AddRecord(ContentCardRecord record) public async Task AddRecord(ContentCardRecord record)
{ {
await db.ContentCards.AddAsync(record); await db.ContentCards.AddAsync(record);
await db.SaveChangesAsync();
} }
public async Task UpdateRecord(ContentCardRecord record) public async Task UpdateRecord(ContentCardRecord record)
@@ -81,5 +72,72 @@ namespace ApplianceRepair
{ {
return await db.BusinessConfig.OrderByDescending(page => page.Id).FirstOrDefaultAsync(); return await db.BusinessConfig.OrderByDescending(page => page.Id).FirstOrDefaultAsync();
} }
public async Task AddRecord(BusinessConfigRecord record)
{
await db.BusinessConfig.AddAsync(record);
await db.SaveChangesAsync();
}
public async Task UpdateRecord(BusinessConfigRecord record)
{
var found = db.BusinessConfig.Where((config) => config.Id == record.Id).FirstOrDefault();
if (found == null)
{
await AddRecord(record);
}
else
{
db.BusinessConfig.Update(record);
}
await db.SaveChangesAsync();
}
}
public class RepairRequestReader(DatabaseContext db)
{
public async Task<RepairRequestRecord?> ReadByRequestNumber(string requestNumber)
{
return await db.RepairRequests.Where((record) => record.RequestNumber == requestNumber).FirstOrDefaultAsync();
}
public async Task<List<RepairRequestRecord>> ReadAll()
{
return await db.RepairRequests.ToListAsync();
}
public async Task AddRecord(RepairRequestRecord record)
{
await db.RepairRequests.AddAsync(record);
await db.SaveChangesAsync();
}
public async Task UpdateRecord(RepairRequestRecord record)
{
var found = db.RepairRequests.Where((config) => config.Id == record.Id).FirstOrDefault();
if (found == null)
{
await AddRecord(record);
}
else
{
db.RepairRequests.Update(record);
}
await db.SaveChangesAsync();
}
}
public class RepairRequestMediaReader(DatabaseContext db)
{
public async Task<List<RepairRequestMediaRecord>> ReadAllByRequestNumber(string requestNumber)
{
return await db.RepairRequestMedia.Where((record) => record.RequestNumber == requestNumber).ToListAsync();
}
public async Task AddRecord(RepairRequestMediaRecord record)
{
await db.RepairRequestMedia.AddAsync(record);
await db.SaveChangesAsync();
}
} }
} }

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB