AspNetForge.WebCore 1.0.3

There is a newer version of this package available.
See the version list below for details.
dotnet add package AspNetForge.WebCore --version 1.0.3
                    
NuGet\Install-Package AspNetForge.WebCore -Version 1.0.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="AspNetForge.WebCore" Version="1.0.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="AspNetForge.WebCore" Version="1.0.3" />
                    
Directory.Packages.props
<PackageReference Include="AspNetForge.WebCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add AspNetForge.WebCore --version 1.0.3
                    
#r "nuget: AspNetForge.WebCore, 1.0.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package AspNetForge.WebCore@1.0.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=AspNetForge.WebCore&version=1.0.3
                    
Install as a Cake Addin
#tool nuget:?package=AspNetForge.WebCore&version=1.0.3
                    
Install as a Cake Tool

AspNetForge.WebCore

Class Library + стартовый шаблон для ASP.NET Core Minimal API (.NET 8).
Содержит generic CRUD-эндпоинты, cookie-авторизацию, встроенные HTML/CSS/JS и готовый пример проекта.

Быстрый старт

dotnet new web -n MyWebApp
cd MyWebApp
dotnet add package AspNetForge.WebCore --version 1.0.3
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11
dotnet build
dotnet run

Важно: пакет рассчитан на чистый проект dotnet new web. Он заменяет Program.cs и копирует wwwroot/.

Что происходит после установки

  1. Models.cs добавляется в проект как linked-файл (компилируется, но не копируется на диск).
  2. Program.cs заменяется шаблонным примером.
  3. wwwroot/ копируется с HTML/CSS/JS.
  4. appsettings.json не перезаписывается, если уже существует. Добавьте строку подключения вручную.

Настройка PostgreSQL

Откройте appsettings.json и добавьте:

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
  }
}

Создайте базу:

CREATE DATABASE examdb;

Встроенные учётные записи

Логин Пароль Роль
admin admin Admin
manager manager Manager
client client Client

Возможности библиотеки

using AspNetForge.Web.Extensions;

app.MapCrud<Product, AppDbContext>("/api/products");
app.MapCrud<Order, AppDbContext>("/api/orders");
app.MapImport<Product, AppDbContext>("/api/products/import").RequireAuthorization("ManagerPlus");
app.UseForgeResources(overwrite: true);

Если файлы не появились

Закройте Visual Studio, удалите папку obj/, откройте решение заново и нажмите Ctrl+Shift+B.

Возможные ошибки

"Строка подключения 'DefaultConnection' не найдена"

Добавьте строку подключения в appsettings.json (см. выше).

Visual Studio предлагает запустить прошлый вариант

  1. Ctrl+Shift+B — собрать проект.
  2. Правый клик по проекту → Перезагрузить проект.
  3. Запустить снова.

CS0234: EntityFrameworkCore не существует

dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 8.0.11

404 Not Found

Убедитесь, что в Program.cs есть:

app.UseDefaultFiles();
app.UseStaticFiles();

wwwroot пустой

При старте UseForgeResources(overwrite: true) копирует встроенные файлы. Убедитесь, что метод вызван до UseStaticFiles().


Fallback: код всех файлов

Если автоматическое копирование не сработало, создайте файлы вручную.

Models.cs

using Microsoft.EntityFrameworkCore;

namespace ExamProject.Models;

public enum UserRole
{
    Client,
    Manager,
    Admin
}

public class User
{
    public int Id { get; set; }
    public required string Login { get; set; }
    public required string Password { get; set; }
    public required string FullName { get; set; }
    public UserRole Role { get; set; } = UserRole.Client;
}

public class PickupPoint
{
    public int Id { get; set; }
    public required string Address { get; set; }
    public required string City { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public required string Article { get; set; }
    public required string Name { get; set; }
    public required string Category { get; set; }
    public required string Manufacturer { get; set; }
    public required string Supplier { get; set; }
    public decimal Price { get; set; }
    public int Discount { get; set; }
    public int Quantity { get; set; }
    public required string Description { get; set; }
    public required string ImagePath { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public int OrderNumber { get; set; }
    public DateTime OrderDate { get; set; }
    public required string ClientFullName { get; set; }
    public int ProductId { get; set; }
    public Product Product { get; set; } = null!;
    public int Quantity { get; set; }
    public int PickupPointId { get; set; }
    public PickupPoint PickupPoint { get; set; } = null!;
    public int Discount { get; set; }
    public required string Status { get; set; }
    public DateTime DeliveryDate { get; set; }
    public int Code { get; set; }
}

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

    public DbSet<User> Users { get; set; }
    public DbSet<PickupPoint> PickupPoints { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
}

Program.cs

using AspNetForge.Web.Extensions;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.EntityFrameworkCore;
using ExamProject.Models;
using System.Security.Claims;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
if (string.IsNullOrWhiteSpace(connectionString))
    throw new InvalidOperationException(
        "Строка подключения 'DefaultConnection' не найдена. " +
        "Добавьте её в appsettings.json.");

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(connectionString));

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
    options.AddPolicy("ManagerPlus", policy => policy.RequireRole("Manager", "Admin"));
});

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.LoginPath = "/login.html";
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.UseForgeResources(overwrite: true);
app.UseDefaultFiles();
app.UseStaticFiles();

app.MapGet("/", () => Results.Redirect("/index.html"));

app.MapCrud<Product, AppDbContext>("/api/products");
app.MapCrud<Order, AppDbContext>("/api/orders");
app.MapCrud<PickupPoint, AppDbContext>("/api/pickuppoints");
app.MapCrud<User, AppDbContext>("/api/users");

app.MapImport<Product, AppDbContext>("/api/products/import").RequireAuthorization("ManagerPlus");

app.MapGet("/api/products/search", async (AppDbContext db, string? q, string? category, string? sort, bool? desc) =>
{
    var query = db.Products.AsQueryable();

    if (!string.IsNullOrWhiteSpace(q))
        query = query.Where(p =>
            p.Name.Contains(q) ||
            p.Article.Contains(q) ||
            p.Manufacturer.Contains(q) ||
            p.Supplier.Contains(q));

    if (!string.IsNullOrWhiteSpace(category))
        query = query.Where(p => p.Category == category);

    query = sort?.ToLower() switch
    {
        "price" => (desc == true) ? query.OrderByDescending(p => p.Price) : query.OrderBy(p => p.Price),
        "discount" => (desc == true) ? query.OrderByDescending(p => p.Discount) : query.OrderBy(p => p.Discount),
        "name" => (desc == true) ? query.OrderByDescending(p => p.Name) : query.OrderBy(p => p.Name),
        _ => query.OrderBy(p => p.Id)
    };

    return Results.Ok(await query.ToListAsync());
});

app.MapGet("/api/categories", async (AppDbContext db) =>
    await db.Products.Select(p => p.Category).Distinct().ToListAsync());

app.MapPost("/login", async (AppDbContext db, HttpContext context) =>
{
    var form = context.Request.Form;
    var login = form["login"].ToString();
    var password = form["password"].ToString();

    var user = await db.Users.FirstOrDefaultAsync(u =>
        u.Login == login && u.Password == password);

    if (user is null)
        return Results.BadRequest("Логин или пароль неверный");

    var claims = new ClaimsIdentity(new[]
    {
        new Claim(ClaimsIdentity.DefaultNameClaimType, user.Login),
        new Claim(ClaimsIdentity.DefaultRoleClaimType, user.Role.ToString()),
        new Claim("FullName", user.FullName)
    }, "Cookies");

    await context.SignInAsync(new ClaimsPrincipal(claims));
    return Results.Redirect("/index.html");
});

app.MapPost("/register", async (AppDbContext db, HttpContext context) =>
{
    var form = context.Request.Form;
    var login = form["login"].ToString();
    var password = form["password"].ToString();

    if (password != form["confirmPassword"].ToString())
        return Results.BadRequest("Пароли не совпадают");

    if (await db.Users.AnyAsync(u => u.Login == login))
        return Results.BadRequest("Пользователь уже существует");

    await db.Users.AddAsync(new User
    {
        Login = login,
        Password = password,
        FullName = form["fullName"].ToString(),
        Role = UserRole.Client
    });

    await db.SaveChangesAsync();
    return Results.Redirect("/login.html");
});

app.MapGet("/logout", async (HttpContext context) =>
{
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    return Results.Redirect("/login.html");
});

app.MapGet("/api/me", (HttpContext context) =>
{
    if (!context.User.Identity!.IsAuthenticated)
        return Results.Json(new { role = "Guest", name = "" });

    return Results.Json(new
    {
        role = context.User.FindFirst(ClaimsIdentity.DefaultRoleClaimType)?.Value ?? "User",
        name = context.User.Identity.Name,
        fullName = context.User.FindFirst("FullName")?.Value ?? ""
    });
});

using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    await db.Database.MigrateAsync();

    if (!db.Users.Any())
    {
        db.Users.AddRange(
            new User { Login = "admin", Password = "admin", FullName = "Администратор", Role = UserRole.Admin },
            new User { Login = "manager", Password = "manager", FullName = "Менеджер", Role = UserRole.Manager },
            new User { Login = "client", Password = "client", FullName = "Клиент", Role = UserRole.Client });
    }

    if (!db.PickupPoints.Any())
    {
        db.PickupPoints.AddRange(
            new PickupPoint { Address = "ул. Ленина, 1", City = "Москва" },
            new PickupPoint { Address = "ул. Гагарина, 5", City = "Санкт-Петербург" });
    }

    if (!db.Products.Any())
    {
        db.Products.AddRange(
            new Product
            {
                Article = "B001",
                Name = "Горный велосипед",
                Category = "Горный",
                Manufacturer = "Stels",
                Supplier = "ООО ВелоСнаб",
                Price = 25000,
                Discount = 10,
                Quantity = 5,
                Description = "Надёжный горный велосипед",
                ImagePath = "images/bike1.jpg"
            },
            new Product
            {
                Article = "B002",
                Name = "Городской велосипед",
                Category = "Городской",
                Manufacturer = "Forward",
                Supplier = "ООО ВелоСнаб",
                Price = 18000,
                Discount = 0,
                Quantity = 12,
                Description = "Удобный городской велосипед",
                ImagePath = "images/bike2.jpg"
            });
    }

    await db.SaveChangesAsync();
}

app.Run();

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=localhost;Port=5432;Database=examdb;Username=postgres;Password=postgres"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*"
}

wwwroot/styles.css

:root {
  --bg: #ffffff;
  --accent: #6A5ACD;
  --accent-dark: #4B0082;
  --highlight: #483D8B;
  --text: #222222;
  --muted: #666666;
  --danger: #dc2626;
  --success: #16a34a;
  --card: #ffffff;
  --border: #e5e7eb;
}

* { box-sizing: border-box; }
body {
  margin: 0;
  font-family: Arial, Helvetica, sans-serif;
  background: var(--bg);
  color: var(--text);
}

header {
  background: var(--accent);
  color: white;
  border-bottom: 4px solid var(--accent-dark);
  padding: 1rem 2rem;
  display: flex;
  justify-content: space-between;
  align-items: center;
}

header h1 { margin: 0; font-size: 1.5rem; }

.logo {
  height: 48px;
  margin-right: 1rem;
}

nav a {
  margin-left: 1rem;
  color: white;
  text-decoration: none;
  font-weight: bold;
}

nav a:hover { text-decoration: underline; }

main {
  max-width: 1300px;
  margin: 2rem auto;
  padding: 0 1rem;
}

.card {
  background: var(--card);
  border: 1px solid var(--border);
  border-radius: .5rem;
  padding: 1.5rem;
  margin-bottom: 1.5rem;
}

.card h3 { margin-top: 0; color: var(--accent-dark); }

input, select, button, textarea {
  padding: .5rem .75rem;
  border: 1px solid var(--border);
  border-radius: .25rem;
  font-family: Arial, Helvetica, sans-serif;
  font-size: 1rem;
}

button {
  background: var(--accent);
  color: white;
  border: none;
  cursor: pointer;
  font-weight: bold;
}

button:hover { background: var(--accent-dark); }
button.secondary { background: var(--muted); }
button.danger { background: var(--danger); }
button.highlight { background: var(--highlight); }

table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 1rem;
}

td, th {
  padding: .75rem;
  border-bottom: 1px solid var(--border);
  text-align: left;
}

th {
  background: var(--accent);
  color: white;
}

tr:hover { background: #f3f4f6; }

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: .75rem;
  margin-bottom: 1rem;
}

.filter-bar {
  display: flex;
  flex-wrap: wrap;
  gap: .75rem;
  align-items: end;
  margin-bottom: 1rem;
}

.auth-form {
  max-width: 360px;
  margin: 5rem auto;
}

.auth-form input {
  width: 100%;
  margin-bottom: .75rem;
}

.badge {
  display: inline-block;
  padding: .25rem .5rem;
  border-radius: .25rem;
  background: var(--accent);
  color: white;
  font-size: .875rem;
}

.hidden { display: none; }

.readonly-row td { background: #f9fafb; }

wwwroot/js/crud.js

const api = {
  async get(url) {
    const r = await fetch(url);
    if (!r.ok) throw new Error('GET ' + url + ' failed: ' + r.status);
    return r.json();
  },
  async post(url, data) {
    const r = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!r.ok) throw new Error('POST failed: ' + r.status);
    return r.json().catch(() => null);
  },
  async put(url, data) {
    const r = await fetch(url, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });
    if (!r.ok) throw new Error('PUT failed: ' + r.status);
    return r.json().catch(() => null);
  },
  async delete(url) {
    const r = await fetch(url, { method: 'DELETE' });
    if (!r.ok) throw new Error('DELETE failed: ' + r.status);
  },
  async postForm(url, formData) {
    const r = await fetch(url, { method: 'POST', body: formData });
    if (!r.ok) throw new Error('POST form failed: ' + r.status);
    return r.json().catch(() => null);
  }
};

async function renderHeader() {
  try {
    const me = await api.get('/api/me');
    const authLink = document.getElementById('authLink');
    const userInfo = document.getElementById('userInfo');
    if (me.role !== 'Guest' && authLink) {
      authLink.textContent = 'Выйти';
      authLink.href = '/logout';
      if (userInfo) userInfo.textContent = (me.fullName || me.name) + ' (' + me.role + ')';
    }
    return me.role;
  } catch {
    return 'Guest';
  }
}

function renderTable(rows, displayKeys, onEdit, onDelete, options = {}) {
  const tbody = document.getElementById('tableBody');
  tbody.innerHTML = '';
  for (const row of rows) {
    const tr = document.createElement('tr');
    if (options.readonly) tr.className = 'readonly-row';
    for (const key of displayKeys) {
      const td = document.createElement('td');
      td.textContent = row[key] ?? '';
      tr.appendChild(td);
    }
    const actions = document.createElement('td');
    if (onEdit) {
      const editBtn = document.createElement('button');
      editBtn.textContent = '✎';
      editBtn.onclick = () => onEdit(row);
      actions.appendChild(editBtn);
    }
    if (onDelete) {
      const delBtn = document.createElement('button');
      delBtn.textContent = '✕';
      delBtn.className = 'danger';
      delBtn.onclick = () => onDelete(row.id);
      actions.appendChild(delBtn);
    }
    tr.appendChild(actions);
    tbody.appendChild(tr);
  }
}

function applyFilters(rows, { search, category, sort, desc }) {
  let result = [...rows];

  if (search) {
    const q = search.toLowerCase();
    result = result.filter(r => Object.values(r).some(v =>
      v != null && v.toString().toLowerCase().includes(q)));
  }

  if (category) {
    result = result.filter(r => r.category === category || r.Category === category);
  }

  if (sort) {
    const key = sort;
    result.sort((a, b) => {
      const av = a[key] ?? '';
      const bv = b[key] ?? '';
      if (typeof av === 'number' && typeof bv === 'number')
        return desc ? bv - av : av - bv;
      return desc ? String(bv).localeCompare(String(av)) : String(av).localeCompare(String(bv));
    });
  }

  return result;
}

wwwroot/index.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Главная — ВелосипедДрайв</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <div style="display:flex;align-items:center;">
      <img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
      <h1>ВелосипедДрайв</h1>
    </div>
    <nav>
      <a href="/products.html">Товары</a>
      <a href="/orders.html">Заказы</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div class="card">
      <h2>Добро пожаловать</h2>
      <p>Это стартовый шаблон на базе <strong>AspNetForge.WebCore</strong>.</p>
      <p>Для входа используйте тестовые учётные записи:</p>
      <ul>
        <li><code>admin / admin</code> — администратор (полный CRUD)</li>
        <li><code>manager / manager</code> — менеджер (товары + фильтры + заказы)</li>
        <li><code>client / client</code> — клиент (только просмотр)</li>
      </ul>
      <p><strong>Важно:</strong> перед запуском создайте базу данных PostgreSQL и проверьте строку подключения в <code>appsettings.json</code>.</p>
    </div>
  </main>
  <script src="/js/crud.js"></script>
  <script>renderHeader();</script>
</body>
</html>

wwwroot/login.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Вход</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <main>
    <div class="card auth-form">
      <h2>Вход</h2>
      <form action="/login" method="post">
        <input name="login" placeholder="Логин" required>
        <input name="password" type="password" placeholder="Пароль" required>
        <button type="submit">Войти</button>
      </form>
      <p>Нет аккаунта? <a href="/register.html">Зарегистрироваться</a></p>
    </div>
  </main>
</body>
</html>

wwwroot/register.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Регистрация</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <main>
    <div class="card auth-form">
      <h2>Регистрация</h2>
      <form action="/register" method="post">
        <input name="login" placeholder="Логин" required>
        <input name="fullName" placeholder="ФИО" required>
        <input name="password" type="password" placeholder="Пароль" required>
        <input name="confirmPassword" type="password" placeholder="Повторите пароль" required>
        <button type="submit">Создать аккаунт</button>
      </form>
      <p>Уже есть аккаунт? <a href="/login.html">Войти</a></p>
    </div>
  </main>
</body>
</html>

wwwroot/products.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Товары</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <div style="display:flex;align-items:center;">
      <img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
      <h1>Товары</h1>
    </div>
    <nav>
      <a href="/index.html">Главная</a>
      <a href="/orders.html">Заказы</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div id="editorCard" class="card hidden">
      <h3>Добавить / изменить</h3>
      <form id="productForm">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="article" name="article" placeholder="Артикул" required>
          <input id="name" name="name" placeholder="Название" required>
          <input id="category" name="category" placeholder="Категория" required>
          <input id="manufacturer" name="manufacturer" placeholder="Производитель" required>
          <input id="supplier" name="supplier" placeholder="Поставщик" required>
          <input id="price" name="price" type="number" step="0.01" placeholder="Цена" required>
          <input id="discount" name="discount" type="number" placeholder="Скидка %" required>
          <input id="quantity" name="quantity" type="number" placeholder="Количество" required>
          <input id="description" name="description" placeholder="Описание" required>
          <input id="imagePath" name="imagePath" placeholder="Путь к изображению" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>

    <div class="card">
      <h3>Список товаров</h3>
      <div class="filter-bar">
        <input id="searchInput" placeholder="Поиск...">
        <select id="categoryFilter"><option value="">Все категории</option></select>
        <select id="sortInput">
          <option value="">Сортировка</option>
          <option value="price">Цена</option>
          <option value="discount">Скидка</option>
          <option value="name">Название</option>
          <option value="quantity">Количество</option>
        </select>
        <label><input type="checkbox" id="descInput"> По убыванию</label>
        <button onclick="applyClientFilters()">Применить</button>
        <button class="secondary" onclick="resetFilters()">Сбросить</button>
      </div>
      <table>
        <thead>
          <tr>
            <th>Артикул</th><th>Название</th><th>Категория</th><th>Производитель</th>
            <th>Поставщик</th><th>Цена</th><th>Скидка</th><th>Кол-во</th><th></th>
          </tr>
        </thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>

    <div id="importCard" class="card hidden">
      <h3>Импорт товаров из CSV</h3>
      <form id="importForm" enctype="multipart/form-data">
        <input type="file" name="file" accept=".csv" required>
        <button type="submit">Импортировать</button>
      </form>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/products';
    const displayKeys = ['article','name','category','manufacturer','supplier','price','discount','quantity'];
    const allKeys = ['id','article','name','category','manufacturer','supplier','price','discount','quantity','description','imagePath'];
    let allRows = [];
    let role = 'Guest';

    async function init() {
      role = await renderHeader();
      if (role === 'Admin' || role === 'Manager') {
        document.getElementById('editorCard').classList.remove('hidden');
      }
      if (role === 'Admin') {
        document.getElementById('importCard').classList.remove('hidden');
      }
      await loadCategories();
      await load();
    }

    async function load() {
      allRows = await api.get(endpoint);
      applyClientFilters();
    }

    async function loadCategories() {
      const cats = await api.get('/api/categories');
      const sel = document.getElementById('categoryFilter');
      cats.forEach(c => {
        const opt = document.createElement('option');
        opt.value = c;
        opt.textContent = c;
        sel.appendChild(opt);
      });
    }

    function applyClientFilters() {
      const search = document.getElementById('searchInput').value;
      const category = document.getElementById('categoryFilter').value;
      const sort = document.getElementById('sortInput').value;
      const desc = document.getElementById('descInput').checked;
      const rows = applyFilters(allRows, { search, category, sort, desc });
      const canEdit = role === 'Admin';
      renderTable(rows, displayKeys, canEdit ? editRow : null, canEdit ? deleteRow : null, { readonly: !canEdit });
    }

    function resetFilters() {
      document.getElementById('searchInput').value = '';
      document.getElementById('categoryFilter').value = '';
      document.getElementById('sortInput').value = '';
      document.getElementById('descInput').checked = false;
      applyClientFilters();
    }

    function editRow(row) {
      allKeys.forEach(k => {
        document.getElementById(k).value = row[k] ?? '';
      });
    }

    function resetForm() { document.getElementById('productForm').reset(); }

    document.getElementById('productForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {};
      allKeys.forEach(k => {
        let v = document.getElementById(k).value;
        if (['id','discount','quantity'].includes(k)) v = Number(v) || 0;
        else if (k === 'price') v = Number(v) || 0;
        payload[k] = v;
      });
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить товар?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    document.getElementById('importForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const formData = new FormData(e.target);
      await api.postForm('/api/products/import', formData);
      e.target.reset();
      await load();
    });

    init();
  </script>
</body>
</html>

wwwroot/orders.html

<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="UTF-8">
  <title>Заказы</title>
  <link rel="stylesheet" href="/styles.css">
</head>
<body>
  <header>
    <div style="display:flex;align-items:center;">
      <img src="/images/logo.png" alt="Логотип" class="logo" onerror="this.style.display='none'">
      <h1>Заказы</h1>
    </div>
    <nav>
      <a href="/index.html">Главная</a>
      <a href="/products.html">Товары</a>
      <span id="userInfo"></span>
      <a id="authLink" href="/login.html">Войти</a>
    </nav>
  </header>
  <main>
    <div id="editorCard" class="card hidden">
      <h3>Добавить / изменить</h3>
      <form id="orderForm">
        <input type="hidden" id="id">
        <div class="form-row">
          <input id="orderNumber" name="orderNumber" type="number" placeholder="Номер заказа" required>
          <input id="orderDate" name="orderDate" type="date" required>
          <input id="clientFullName" name="clientFullName" placeholder="ФИО клиента" required>
          <input id="productId" name="productId" type="number" placeholder="ID товара" required>
          <input id="quantity" name="quantity" type="number" placeholder="Количество" required>
          <input id="pickupPointId" name="pickupPointId" type="number" placeholder="ID пункта выдачи" required>
          <input id="discount" name="discount" type="number" placeholder="Скидка %" required>
          <input id="status" name="status" placeholder="Статус" required>
          <input id="deliveryDate" name="deliveryDate" type="date" required>
          <input id="code" name="code" type="number" placeholder="Код получения" required>
        </div>
        <button type="submit">Сохранить</button>
        <button type="button" class="secondary" onclick="resetForm()">Отмена</button>
      </form>
    </div>

    <div class="card">
      <h3>Список заказов</h3>
      <table>
        <thead>
          <tr>
            <th>№</th><th>Дата</th><th>Клиент</th><th>Товар ID</th><th>Кол-во</th>
            <th>Пункт выдачи ID</th><th>Скидка</th><th>Статус</th><th>Доставка</th><th>Код</th><th></th>
          </tr>
        </thead>
        <tbody id="tableBody"></tbody>
      </table>
    </div>
  </main>

  <script src="/js/crud.js"></script>
  <script>
    const endpoint = '/api/orders';
    const displayKeys = ['orderNumber','orderDate','clientFullName','productId','quantity','pickupPointId','discount','status','deliveryDate','code'];
    const allKeys = ['id','orderNumber','orderDate','clientFullName','productId','quantity','pickupPointId','discount','status','deliveryDate','code'];
    let role = 'Guest';

    async function init() {
      role = await renderHeader();
      if (role === 'Admin') {
        document.getElementById('editorCard').classList.remove('hidden');
      }
      await load();
    }

    async function load() {
      const rows = await api.get(endpoint);
      const canEdit = role === 'Admin';
      renderTable(rows, displayKeys, canEdit ? editRow : null, canEdit ? deleteRow : null, { readonly: !canEdit });
    }

    function editRow(row) {
      allKeys.forEach(k => {
        document.getElementById(k).value = row[k] ? row[k].toString().split('T')[0] : '';
      });
    }

    function resetForm() { document.getElementById('orderForm').reset(); }

    document.getElementById('orderForm').addEventListener('submit', async (e) => {
      e.preventDefault();
      const payload = {};
      allKeys.forEach(k => {
        let v = document.getElementById(k).value;
        if (['id','orderNumber','productId','quantity','pickupPointId','discount','code'].includes(k)) v = Number(v) || 0;
        else if (['orderDate','deliveryDate'].includes(k)) v = new Date(v).toISOString();
        payload[k] = v;
      });
      if (payload.id) await api.put(`${endpoint}/${payload.id}`, payload);
      else await api.post(endpoint, payload);
      resetForm();
      await load();
    });

    async function deleteRow(id) {
      if (!confirm('Удалить заказ?')) return;
      await api.delete(`${endpoint}/${id}`);
      await load();
    }

    init();
  </script>
</body>
</html>
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 128 6/15/2026
1.0.9 120 6/14/2026
1.0.8 116 6/14/2026
1.0.7 116 6/14/2026
1.0.6 112 6/14/2026
1.0.5 116 6/14/2026
1.0.4 114 6/14/2026
1.0.3 116 6/13/2026
1.0.2 119 6/12/2026
1.0.1 119 6/12/2026
1.0.0 115 6/12/2026