Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7955f0e1f | |||
| 4661922633 | |||
| fcf0cf0b77 | |||
| e634123904 | |||
| df9bce64f7 | |||
| 60aab78cd3 | |||
| dd779e58f4 |
@@ -0,0 +1,25 @@
|
||||
## Description
|
||||
|
||||
<!-- What does this PR do and why? -->
|
||||
|
||||
## Changes
|
||||
|
||||
<!-- Key changes made -->
|
||||
|
||||
-
|
||||
-
|
||||
|
||||
## How to Test
|
||||
|
||||
1. Run the Blazor Web App: `dotnet run --project Fixiy.Web/Fixiy.Web.csproj`
|
||||
2. <!-- Additional steps specific to this PR -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] New interfaces defined in `Fixiy.Shared/Interfaces/` (not in platform projects)
|
||||
- [ ] Platform-specific services implemented in both `Fixiy.Maui/Services/` and `Fixiy.Web/Services/`
|
||||
- [ ] Both DI roots updated (`MauiProgram.cs` and `Fixiy.Web/Program.cs`)
|
||||
- [ ] Render modes use `InteractiveRenderSettings.*` properties, not `RenderMode.*` constants
|
||||
- [ ] No MAUI-specific APIs (`DeviceInfo`, `FileSystem`, etc.) introduced in `Fixiy.Shared`
|
||||
- [ ] `_Imports.razor` updated if new shared namespaces are needed
|
||||
- [ ] `InteractiveRenderSettings.ConfigureBlazorHybridRenderModes()` updated if new render mode properties added
|
||||
@@ -0,0 +1,450 @@
|
||||
# GitHub Copilot Instructions — Fixiy
|
||||
|
||||
## Project Overview
|
||||
|
||||
Fixiy is a MAUI Blazor Hybrid application with a shared codebase.
|
||||
|
||||
### Solution Structure
|
||||
|
||||
* `Fixiy.Shared` contains:
|
||||
|
||||
* Business logic
|
||||
* Razor pages
|
||||
* Shared components
|
||||
* DTOs
|
||||
* Interfaces
|
||||
* Service abstractions
|
||||
|
||||
* `Fixiy.Maui` is the MAUI host application.
|
||||
|
||||
* `Fixiy.Web` is the ASP.NET Core host application.
|
||||
|
||||
The Shared project is the source of truth. Avoid duplicating logic between hosts.
|
||||
|
||||
---
|
||||
|
||||
## Agent Behaviour
|
||||
|
||||
* Before making changes, inspect the existing implementation and follow established patterns.
|
||||
* Prefer extending existing functionality over creating new implementations.
|
||||
* Make the smallest change necessary to satisfy the requirement.
|
||||
* Never rewrite large portions of working code unless explicitly requested.
|
||||
* Search for existing services, DTOs, components, pages and interfaces before creating new ones.
|
||||
* Never create duplicate functionality.
|
||||
* Do not rename public classes, interfaces, routes, pages or services without verifying all usages.
|
||||
* When requirements are unclear, ask for clarification instead of making assumptions.
|
||||
* Preserve the existing architecture.
|
||||
* Do not move files between projects unless explicitly requested.
|
||||
* Do not generate placeholder implementations containing TODO comments unless specifically requested.
|
||||
* Always check whether a change affects both MAUI and Web hosts.
|
||||
|
||||
---
|
||||
|
||||
## Language and Style
|
||||
|
||||
* Use C# 12+.
|
||||
* Always keep:
|
||||
|
||||
```xml
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
```
|
||||
|
||||
* Never disable nullable reference types without strong justification.
|
||||
* Use file-scoped namespaces:
|
||||
|
||||
```csharp
|
||||
namespace Fixiy.Shared.Services;
|
||||
```
|
||||
|
||||
* Never use block-scoped namespaces.
|
||||
* Prefer `record` types for immutable DTOs.
|
||||
* Use expression-bodied members when they improve readability.
|
||||
* Use collection expressions when appropriate.
|
||||
* Use `async`/`await` consistently.
|
||||
* Never use `.Result`, `.Wait()` or `.GetAwaiter().GetResult()`.
|
||||
* Remove unused `using` directives.
|
||||
* One type per file.
|
||||
* File names must match type names.
|
||||
* Constants use PascalCase.
|
||||
* Do not use Hungarian notation.
|
||||
* Omit the `private` modifier on fields.
|
||||
|
||||
---
|
||||
|
||||
## Blazor Component Conventions
|
||||
|
||||
### Component Locations
|
||||
|
||||
Shared components must live inside:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Components/
|
||||
```
|
||||
|
||||
Never create reusable components in:
|
||||
|
||||
```text
|
||||
Fixiy.Maui
|
||||
Fixiy.Web
|
||||
```
|
||||
|
||||
### Structure
|
||||
|
||||
Pages:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Components/Pages/
|
||||
```
|
||||
|
||||
Layouts:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Components/Layout/
|
||||
```
|
||||
|
||||
Reusable elements:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Components/SingleElements/
|
||||
```
|
||||
|
||||
### Styling
|
||||
|
||||
* Scoped styles must use matching `.razor.css` files.
|
||||
* Keep component-specific styling inside scoped CSS whenever possible.
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
Use:
|
||||
|
||||
```razor
|
||||
@inject IService Service
|
||||
```
|
||||
|
||||
Do not attempt constructor injection in Razor components.
|
||||
|
||||
### Parameters
|
||||
|
||||
* Prefer `[Parameter]`.
|
||||
* Avoid `CascadingParameter` unless truly necessary across multiple levels.
|
||||
|
||||
### Code Organization
|
||||
|
||||
* Keep `@code` blocks small.
|
||||
* Extract complex logic into:
|
||||
|
||||
* `.razor.cs`
|
||||
* Service classes
|
||||
* ViewModels
|
||||
|
||||
---
|
||||
|
||||
## Render Mode Rules
|
||||
|
||||
Always use:
|
||||
|
||||
```razor
|
||||
@InteractiveServer
|
||||
@InteractiveAuto
|
||||
@InteractiveWebAssembly
|
||||
```
|
||||
|
||||
through:
|
||||
|
||||
```csharp
|
||||
InteractiveRenderSettings
|
||||
```
|
||||
|
||||
Never use:
|
||||
|
||||
```csharp
|
||||
RenderMode.Server
|
||||
RenderMode.Auto
|
||||
RenderMode.WebAssembly
|
||||
```
|
||||
|
||||
directly.
|
||||
|
||||
### Important
|
||||
|
||||
`Fixiy.Maui/MauiProgram.cs` calls:
|
||||
|
||||
```csharp
|
||||
ConfigureBlazorHybridRenderModes()
|
||||
```
|
||||
|
||||
which intentionally sets render modes to `null`.
|
||||
|
||||
This is required for MAUI Blazor Hybrid.
|
||||
|
||||
Do not:
|
||||
|
||||
* Replace null render modes
|
||||
* Add workarounds
|
||||
* Introduce MAUI-specific render mode hacks
|
||||
|
||||
All shared components must tolerate null render modes.
|
||||
|
||||
---
|
||||
|
||||
## MAUI Blazor Hybrid Rules
|
||||
|
||||
* Navigation must remain compatible with iOS swipe-back gestures.
|
||||
|
||||
* Prefer native MAUI navigation for secondary pages.
|
||||
|
||||
* Use PushAsync for stack navigation.
|
||||
|
||||
* Do not introduce JavaScript solutions when a MAUI or Blazor solution exists.
|
||||
|
||||
* Shared components must work identically in:
|
||||
|
||||
* MAUI Hybrid
|
||||
* ASP.NET Core Server
|
||||
* WebAssembly
|
||||
|
||||
* Never introduce host-specific code into shared components.
|
||||
|
||||
---
|
||||
|
||||
## MVVM Guidelines
|
||||
|
||||
* Business logic must not live inside Razor pages.
|
||||
* Complex state management belongs in services or ViewModels.
|
||||
* UI components should focus on presentation.
|
||||
* Keep ViewModels platform-independent.
|
||||
* Avoid code duplication between ViewModels and services.
|
||||
|
||||
---
|
||||
|
||||
## IFormFactor Platform Abstraction
|
||||
|
||||
Platform-specific functionality must be accessed through:
|
||||
|
||||
```csharp
|
||||
IFormFactor
|
||||
```
|
||||
|
||||
located in:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Interfaces/
|
||||
```
|
||||
|
||||
### Forbidden in Shared
|
||||
|
||||
Never directly access:
|
||||
|
||||
```csharp
|
||||
DeviceInfo
|
||||
Connectivity
|
||||
FileSystem
|
||||
Permissions
|
||||
Launcher
|
||||
Preferences
|
||||
SecureStorage
|
||||
Microsoft.Maui.*
|
||||
```
|
||||
|
||||
from:
|
||||
|
||||
```text
|
||||
Fixiy.Shared
|
||||
```
|
||||
|
||||
### Adding New Platform Features
|
||||
|
||||
Always follow this process:
|
||||
|
||||
1. Create interface in:
|
||||
|
||||
```text
|
||||
Fixiy.Shared/Interfaces/
|
||||
```
|
||||
|
||||
2. Implement in:
|
||||
|
||||
```text
|
||||
Fixiy.Maui/Services/
|
||||
```
|
||||
|
||||
3. Implement in:
|
||||
|
||||
```text
|
||||
Fixiy.Web/Services/
|
||||
```
|
||||
|
||||
4. Register in both DI roots.
|
||||
|
||||
Do not skip any step.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Injection
|
||||
|
||||
### Service Registration
|
||||
|
||||
Always verify registrations in both hosts.
|
||||
|
||||
### Lifetimes
|
||||
|
||||
#### MAUI
|
||||
|
||||
Use:
|
||||
|
||||
```csharp
|
||||
Singleton
|
||||
```
|
||||
|
||||
for platform services.
|
||||
|
||||
#### Web
|
||||
|
||||
Use:
|
||||
|
||||
```csharp
|
||||
Scoped
|
||||
```
|
||||
|
||||
for platform services.
|
||||
|
||||
### Interfaces
|
||||
|
||||
Interfaces must not assume any specific lifetime.
|
||||
|
||||
### Injection
|
||||
|
||||
Prefer:
|
||||
|
||||
```csharp
|
||||
IService
|
||||
```
|
||||
|
||||
over concrete implementations.
|
||||
|
||||
Never inject a concrete type when an abstraction exists.
|
||||
|
||||
---
|
||||
|
||||
## IntegryApiClient Registration
|
||||
|
||||
Use the same configuration in both hosts.
|
||||
|
||||
### MAUI
|
||||
|
||||
```csharp
|
||||
.UseIntegry(appToken, useLoginAzienda: true)
|
||||
```
|
||||
|
||||
### Web
|
||||
|
||||
```csharp
|
||||
builder.Services.UseIntegry(
|
||||
appToken,
|
||||
useLoginAzienda: true)
|
||||
```
|
||||
|
||||
### Rules
|
||||
|
||||
* One registration per host.
|
||||
* Never register twice.
|
||||
* Always keep app tokens aligned.
|
||||
* Always keep versions aligned.
|
||||
|
||||
---
|
||||
|
||||
## Data Access Guidelines
|
||||
|
||||
### Entity Framework
|
||||
|
||||
* Prefer async APIs.
|
||||
* Use `AsNoTracking()` for read-only queries.
|
||||
* Avoid loading entire tables into memory.
|
||||
* Avoid N+1 query patterns.
|
||||
* Prefer projections over loading full entities when possible.
|
||||
|
||||
### Performance
|
||||
|
||||
* Minimize allocations.
|
||||
* Reuse existing services.
|
||||
* Avoid unnecessary LINQ enumerations.
|
||||
* Prefer server-side filtering.
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
* Use exceptions only for exceptional situations.
|
||||
* Validate inputs early.
|
||||
* Log meaningful information.
|
||||
* Never swallow exceptions silently.
|
||||
* Preserve stack traces.
|
||||
|
||||
Do not write:
|
||||
|
||||
```csharp
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintenance Matrix
|
||||
|
||||
| Change | Cascades To |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------- |
|
||||
| Add/modify `Fixiy.Shared/Interfaces/*.cs` | Update MAUI implementation + Web implementation + DI registration |
|
||||
| Modify `InteractiveRenderSettings.cs` | Update `ConfigureBlazorHybridRenderModes()` |
|
||||
| Add shared page | Add navigation entry if user accessible |
|
||||
| Add shared service | Verify registration in both hosts |
|
||||
| Add platform capability | Interface + MAUI implementation + Web implementation |
|
||||
| Add NuGet package | Verify target project and feed availability |
|
||||
| Change IntegryApiClient version | Update all project references |
|
||||
| Modify `_Imports.razor` | Ensure no host-specific namespaces are introduced |
|
||||
| Update app token | Update both hosts |
|
||||
|
||||
---
|
||||
|
||||
## Testing Conventions
|
||||
|
||||
Current solution has no dedicated test projects.
|
||||
|
||||
When adding tests:
|
||||
|
||||
### Frameworks
|
||||
|
||||
Use:
|
||||
|
||||
* xUnit
|
||||
* bUnit
|
||||
* Moq or NSubstitute
|
||||
|
||||
### Requirements
|
||||
|
||||
* Test projects should reference `Fixiy.Shared`.
|
||||
* Mock `IFormFactor`.
|
||||
* Avoid manual stubs unless interfaces are trivial.
|
||||
|
||||
---
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
Before completing a task verify:
|
||||
|
||||
* Existing patterns were followed.
|
||||
* No duplicate services were created.
|
||||
* No duplicate DTOs were created.
|
||||
* No duplicate pages were created.
|
||||
* No duplicate components were created.
|
||||
* Shared code contains no MAUI-specific APIs.
|
||||
* DI registrations are correct.
|
||||
* Both hosts were updated when required.
|
||||
* Nullable reference types remain enabled.
|
||||
* Async APIs are used correctly.
|
||||
* Render mode rules were respected.
|
||||
* Navigation compatibility remains intact.
|
||||
* No unnecessary files were added.
|
||||
* No working code was rewritten unnecessarily.
|
||||
* The solution architecture remains consistent.
|
||||
@@ -0,0 +1,822 @@
|
||||
---
|
||||
name: blazor-specialist
|
||||
description: Blazor UI development specialist. Use for building Blazor Server or WebAssembly apps, component development, state management, or form handling.
|
||||
---
|
||||
|
||||
# Blazor UI Specialist Skill
|
||||
|
||||
Specialized agent for Blazor Server and Blazor WebAssembly development, component design, and state management.
|
||||
|
||||
## Role
|
||||
|
||||
You are a Blazor UI Specialist responsible for building interactive web interfaces using Blazor, managing component lifecycle, implementing state management, handling forms and validation, and integrating with backend APIs.
|
||||
|
||||
## Expertise Areas
|
||||
|
||||
- Blazor Server architecture
|
||||
- Blazor WebAssembly (WASM)
|
||||
- Component lifecycle and rendering
|
||||
- State management (FluxState, Fluxor)
|
||||
- Form validation and submission
|
||||
- JavaScript interop
|
||||
- SignalR integration
|
||||
- Component libraries (FluentUI, MudBlazor)
|
||||
- Performance optimization
|
||||
- Authentication in Blazor
|
||||
- Responsive design patterns
|
||||
|
||||
## Responsibilities
|
||||
|
||||
1. **Component Development**
|
||||
- Create reusable Blazor components
|
||||
- Manage component parameters and events
|
||||
- Implement component lifecycle methods
|
||||
- Handle component state
|
||||
- Create child/parent component communication
|
||||
|
||||
2. **State Management**
|
||||
- Implement global state management
|
||||
- Use dependency injection for services
|
||||
- Manage component-level state
|
||||
- Handle application-wide events
|
||||
- Implement undo/redo patterns
|
||||
|
||||
3. **Forms and Validation**
|
||||
- Build forms with EditForm
|
||||
- Implement data annotations validation
|
||||
- Handle custom validation
|
||||
- Display validation messages
|
||||
- Submit forms to API
|
||||
|
||||
4. **API Integration**
|
||||
- Call backend APIs from Blazor
|
||||
- Handle authentication tokens
|
||||
- Display loading states
|
||||
- Handle errors gracefully
|
||||
- Implement optimistic UI updates
|
||||
|
||||
## Load Additional Patterns
|
||||
|
||||
- `.claude/patterns/api-patterns.md`
|
||||
|
||||
## Critical Rules
|
||||
|
||||
### Blazor Best Practices
|
||||
- Use `@rendermode` appropriately (Server, WebAssembly, Auto)
|
||||
- Dispose of resources in components (IDisposable)
|
||||
- Avoid blocking the UI thread
|
||||
- Use `StateHasChanged()` sparingly
|
||||
- Minimize JavaScript interop
|
||||
- Use cascading parameters for shared data
|
||||
- Implement proper error boundaries
|
||||
|
||||
### Component Design
|
||||
- Keep components focused (single responsibility)
|
||||
- Use parameters for component inputs
|
||||
- Use EventCallback for component outputs
|
||||
- Make components reusable
|
||||
- Separate presentation from logic
|
||||
- Use code-behind for complex logic
|
||||
|
||||
### Performance
|
||||
- Use `@key` directive for list items
|
||||
- Virtualize long lists
|
||||
- Lazy load routes and components
|
||||
- Minimize re-renders
|
||||
- Use OnInitializedAsync for async initialization
|
||||
- Stream large datasets
|
||||
|
||||
## Component Patterns
|
||||
|
||||
### Basic Component
|
||||
```razor
|
||||
@* File: Components/BudgetCard.razor *@
|
||||
@namespace {ApplicationName}.UI.Components
|
||||
|
||||
<div class="budget-card">
|
||||
<h3>@Budget.Name</h3>
|
||||
<p class="amount">@Budget.Amount.ToString("C")</p>
|
||||
<p class="created">Created: @Budget.CreatedDate.ToString("d")</p>
|
||||
|
||||
<button @onclick="OnEditClicked">Edit</button>
|
||||
<button @onclick="OnDeleteClicked" class="danger">Delete</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired]
|
||||
public BudgetResponse Budget { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<BudgetResponse> OnEdit { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback<Guid> OnDelete { get; set; }
|
||||
|
||||
private async Task OnEditClicked()
|
||||
{
|
||||
if (OnEdit.HasDelegate)
|
||||
await OnEdit.InvokeAsync(Budget);
|
||||
}
|
||||
|
||||
private async Task OnDeleteClicked()
|
||||
{
|
||||
if (OnDelete.HasDelegate)
|
||||
await OnDelete.InvokeAsync(Budget.BudgetId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component with Code-Behind
|
||||
```razor
|
||||
@* File: Pages/Budgets/BudgetList.razor *@
|
||||
@page "/budgets"
|
||||
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||
@inherits BudgetListBase
|
||||
|
||||
<PageTitle>Budgets</PageTitle>
|
||||
|
||||
<div class="budget-list-page">
|
||||
<h1>Budgets</h1>
|
||||
|
||||
@if (IsLoading)
|
||||
{
|
||||
<p>Loading budgets...</p>
|
||||
}
|
||||
else if (ErrorMessage is not null)
|
||||
{
|
||||
<div class="error">@ErrorMessage</div>
|
||||
}
|
||||
else if (!Budgets.Any())
|
||||
{
|
||||
<p>No budgets found. Create your first budget!</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="budget-grid">
|
||||
@foreach (var budget in Budgets)
|
||||
{
|
||||
<BudgetCard
|
||||
Budget="@budget"
|
||||
OnEdit="@HandleEditBudget"
|
||||
OnDelete="@HandleDeleteBudget" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<button @onclick="HandleCreateBudget" class="primary">Create Budget</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```csharp
|
||||
// File: Pages/Budgets/BudgetList.razor.cs
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using {ApplicationName}.UI.Services;
|
||||
|
||||
namespace {ApplicationName}.UI.Pages.Budgets;
|
||||
|
||||
public class BudgetListBase : ComponentBase, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
protected IBudgetService BudgetService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected ILogger<BudgetListBase> Logger { get; set; } = default!;
|
||||
|
||||
protected List<BudgetResponse> Budgets { get; set; } = new();
|
||||
protected bool IsLoading { get; set; }
|
||||
protected string? ErrorMessage { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await LoadBudgetsAsync();
|
||||
}
|
||||
|
||||
protected async Task LoadBudgetsAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
Budgets = await BudgetService.GetBudgetsAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading budgets");
|
||||
ErrorMessage = "Failed to load budgets. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected void HandleCreateBudget()
|
||||
{
|
||||
Navigation.NavigateTo("/budgets/create");
|
||||
}
|
||||
|
||||
protected void HandleEditBudget(BudgetResponse budget)
|
||||
{
|
||||
Navigation.NavigateTo($"/budgets/{budget.BudgetId}/edit");
|
||||
}
|
||||
|
||||
protected async Task HandleDeleteBudget(Guid budgetId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await BudgetService.DeleteBudgetAsync(budgetId);
|
||||
await LoadBudgetsAsync(); // Reload list
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error deleting budget {BudgetId}", budgetId);
|
||||
ErrorMessage = "Failed to delete budget. Please try again.";
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Clean up subscriptions, timers, etc.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Form Validation Pattern
|
||||
|
||||
```razor
|
||||
@* File: Pages/Budgets/CreateBudget.razor *@
|
||||
@page "/budgets/create"
|
||||
@namespace {ApplicationName}.UI.Pages.Budgets
|
||||
|
||||
<PageTitle>Create Budget</PageTitle>
|
||||
|
||||
<div class="create-budget-page">
|
||||
<h1>Create Budget</h1>
|
||||
|
||||
<EditForm Model="@Model" OnValidSubmit="@HandleValidSubmit" FormName="CreateBudgetForm">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name:</label>
|
||||
<InputText id="name" @bind-Value="Model.Name" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.Name)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="amount">Amount:</label>
|
||||
<InputNumber id="amount" @bind-Value="Model.Amount" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.Amount)" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="startDate">Start Date:</label>
|
||||
<InputDate id="startDate" @bind-Value="Model.StartDate" class="form-control" />
|
||||
<ValidationMessage For="@(() => Model.StartDate)" />
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" disabled="@IsSubmitting">
|
||||
@if (IsSubmitting)
|
||||
{
|
||||
<span>Creating...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Create</span>
|
||||
}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
|
||||
</div>
|
||||
|
||||
@if (ErrorMessage is not null)
|
||||
{
|
||||
<div class="alert alert-danger mt-3">@ErrorMessage</div>
|
||||
}
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Inject]
|
||||
private IBudgetService BudgetService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private NavigationManager Navigation { get; set; } = default!;
|
||||
|
||||
[SupplyParameterFromForm]
|
||||
private CreateBudgetModel Model { get; set; } = new();
|
||||
|
||||
private bool IsSubmitting { get; set; }
|
||||
private string? ErrorMessage { get; set; }
|
||||
|
||||
private async Task HandleValidSubmit()
|
||||
{
|
||||
IsSubmitting = true;
|
||||
ErrorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var command = new CreateBudgetCommand(
|
||||
Model.Name,
|
||||
Model.Amount,
|
||||
Model.StartDate);
|
||||
|
||||
var result = await BudgetService.CreateBudgetAsync(command);
|
||||
|
||||
Navigation.NavigateTo($"/budgets/{result.BudgetId}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = "Failed to create budget. Please try again.";
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
Navigation.NavigateTo("/budgets");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
// File: Models/CreateBudgetModel.cs
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
public class CreateBudgetModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(100, MinimumLength = 3)]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
[Range(0.01, 1_000_000)]
|
||||
public decimal Amount { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTimeOffset StartDate { get; set; } = DateTimeOffset.Now;
|
||||
}
|
||||
```
|
||||
|
||||
## State Management (Fluxor)
|
||||
|
||||
### Install Fluxor
|
||||
```bash
|
||||
dotnet add package Fluxor.Blazor.Web
|
||||
```
|
||||
|
||||
### Define State
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetState.cs
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public record BudgetState
|
||||
{
|
||||
public List<BudgetResponse> Budgets { get; init; } = new();
|
||||
public bool IsLoading { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
}
|
||||
```
|
||||
|
||||
### Define Actions
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetActions.cs
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public record LoadBudgetsAction;
|
||||
public record LoadBudgetsSuccessAction(List<BudgetResponse> Budgets);
|
||||
public record LoadBudgetsFailureAction(string ErrorMessage);
|
||||
```
|
||||
|
||||
### Implement Reducer
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetReducers.cs
|
||||
using Fluxor;
|
||||
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public static class BudgetReducers
|
||||
{
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsAction(BudgetState state, LoadBudgetsAction action) =>
|
||||
state with { IsLoading = true, ErrorMessage = null };
|
||||
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsSuccessAction(BudgetState state, LoadBudgetsSuccessAction action) =>
|
||||
state with { IsLoading = false, Budgets = action.Budgets };
|
||||
|
||||
[ReducerMethod]
|
||||
public static BudgetState ReduceLoadBudgetsFailureAction(BudgetState state, LoadBudgetsFailureAction action) =>
|
||||
state with { IsLoading = false, ErrorMessage = action.ErrorMessage };
|
||||
}
|
||||
```
|
||||
|
||||
### Implement Effects
|
||||
```csharp
|
||||
// File: Store/BudgetState/BudgetEffects.cs
|
||||
using Fluxor;
|
||||
|
||||
namespace {ApplicationName}.UI.Store.BudgetState;
|
||||
|
||||
public class BudgetEffects(IBudgetService budgetService)
|
||||
{
|
||||
[EffectMethod]
|
||||
public async Task HandleLoadBudgetsAction(LoadBudgetsAction action, IDispatcher dispatcher)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budgets = await budgetService.GetBudgetsAsync();
|
||||
dispatcher.Dispatch(new LoadBudgetsSuccessAction(budgets));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
dispatcher.Dispatch(new LoadBudgetsFailureAction(ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Register Fluxor
|
||||
```csharp
|
||||
// Program.cs
|
||||
builder.Services.AddFluxor(options =>
|
||||
{
|
||||
options.ScanAssemblies(typeof(Program).Assembly);
|
||||
options.UseReduxDevTools();
|
||||
});
|
||||
```
|
||||
|
||||
### Use in Component
|
||||
```razor
|
||||
@inherits FluxorComponent
|
||||
@inject IState<BudgetState> BudgetState
|
||||
@inject IDispatcher Dispatcher
|
||||
|
||||
<div>
|
||||
@if (BudgetState.Value.IsLoading)
|
||||
{
|
||||
<p>Loading...</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var budget in BudgetState.Value.Budgets)
|
||||
{
|
||||
<BudgetCard Budget="@budget" />
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
Dispatcher.Dispatch(new LoadBudgetsAction());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Service Pattern
|
||||
|
||||
```csharp
|
||||
// File: Services/BudgetService.cs
|
||||
namespace {ApplicationName}.UI.Services;
|
||||
|
||||
using System.Net.Http.Json;
|
||||
|
||||
public interface IBudgetService
|
||||
{
|
||||
Task<List<BudgetResponse>> GetBudgetsAsync(CancellationToken cancellationToken = default);
|
||||
Task<BudgetResponse> GetBudgetByIdAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
Task<CreateBudgetResponse> CreateBudgetAsync(CreateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||
Task UpdateBudgetAsync(Guid id, UpdateBudgetCommand command, CancellationToken cancellationToken = default);
|
||||
Task DeleteBudgetAsync(Guid id, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public class BudgetService(
|
||||
HttpClient httpClient,
|
||||
ILogger<BudgetService> logger
|
||||
) : IBudgetService
|
||||
{
|
||||
public async Task<List<BudgetResponse>> GetBudgetsAsync(
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budgets = await httpClient.GetFromJsonAsync<List<BudgetResponse>>(
|
||||
"/budgets",
|
||||
cancellationToken);
|
||||
|
||||
return budgets ?? new List<BudgetResponse>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error fetching budgets");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<BudgetResponse> GetBudgetByIdAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var budget = await httpClient.GetFromJsonAsync<BudgetResponse>(
|
||||
$"/budgets/{id}",
|
||||
cancellationToken);
|
||||
|
||||
return budget ?? throw new InvalidOperationException("Budget not found");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error fetching budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<CreateBudgetResponse> CreateBudgetAsync(
|
||||
CreateBudgetCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PostAsJsonAsync(
|
||||
"/budgets",
|
||||
command,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var result = await response.Content.ReadFromJsonAsync<CreateBudgetResponse>(
|
||||
cancellationToken: cancellationToken);
|
||||
|
||||
return result ?? throw new InvalidOperationException("Failed to create budget");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error creating budget");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateBudgetAsync(
|
||||
Guid id,
|
||||
UpdateBudgetCommand command,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.PutAsJsonAsync(
|
||||
$"/budgets/{id}",
|
||||
command,
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error updating budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task DeleteBudgetAsync(
|
||||
Guid id,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await httpClient.DeleteAsync(
|
||||
$"/budgets/{id}",
|
||||
cancellationToken);
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Error deleting budget {BudgetId}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register service
|
||||
builder.Services.AddScoped<IBudgetService, BudgetService>();
|
||||
```
|
||||
|
||||
## JavaScript Interop
|
||||
|
||||
```razor
|
||||
@inject IJSRuntime JS
|
||||
|
||||
<button @onclick="ShowAlert">Show Alert</button>
|
||||
<button @onclick="GetLocalStorage">Get from LocalStorage</button>
|
||||
|
||||
@code {
|
||||
private async Task ShowAlert()
|
||||
{
|
||||
await JS.InvokeVoidAsync("alert", "Hello from Blazor!");
|
||||
}
|
||||
|
||||
private async Task GetLocalStorage()
|
||||
{
|
||||
var value = await JS.InvokeAsync<string>("localStorage.getItem", "myKey");
|
||||
Console.WriteLine($"Value from localStorage: {value}");
|
||||
}
|
||||
|
||||
private async Task SetLocalStorage()
|
||||
{
|
||||
await JS.InvokeVoidAsync("localStorage.setItem", "myKey", "myValue");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// wwwroot/js/interop.js
|
||||
window.budgetApp = {
|
||||
formatCurrency: function (amount) {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
}).format(amount);
|
||||
},
|
||||
|
||||
showConfirmDialog: function (message) {
|
||||
return confirm(message);
|
||||
},
|
||||
|
||||
downloadFile: function (filename, content) {
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Authentication in Blazor
|
||||
|
||||
```razor
|
||||
@* File: Pages/Login.razor *@
|
||||
@page "/login"
|
||||
@inject NavigationManager Navigation
|
||||
@inject IAuthenticationService AuthService
|
||||
|
||||
<div class="login-page">
|
||||
<h1>Login</h1>
|
||||
|
||||
<EditForm Model="@loginModel" OnValidSubmit="@HandleLogin">
|
||||
<DataAnnotationsValidator />
|
||||
<ValidationSummary />
|
||||
|
||||
<div class="form-group">
|
||||
<label>Email:</label>
|
||||
<InputText @bind-Value="loginModel.Email" class="form-control" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Password:</label>
|
||||
<InputText type="password" @bind-Value="loginModel.Password" class="form-control" />
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Login</button>
|
||||
</EditForm>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private LoginModel loginModel = new();
|
||||
|
||||
private async Task HandleLogin()
|
||||
{
|
||||
var result = await AuthService.LoginAsync(loginModel.Email, loginModel.Password);
|
||||
|
||||
if (result.Success)
|
||||
{
|
||||
Navigation.NavigateTo("/");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```razor
|
||||
@* File: Components/AuthorizeView.razor *@
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<p>Hello, @context.User.Identity?.Name!</p>
|
||||
<a href="/logout">Logout</a>
|
||||
</Authorized>
|
||||
<NotAuthorized>
|
||||
<a href="/login">Login</a>
|
||||
</NotAuthorized>
|
||||
</AuthorizeView>
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Virtualization
|
||||
```razor
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
|
||||
<Virtualize Items="@budgets" Context="budget">
|
||||
<BudgetCard Budget="@budget" />
|
||||
</Virtualize>
|
||||
```
|
||||
|
||||
### Lazy Loading
|
||||
```razor
|
||||
@* File: App.razor *@
|
||||
<Router AppAssembly="@typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
</Found>
|
||||
<NotFound>
|
||||
<PageTitle>Not found</PageTitle>
|
||||
<p>Sorry, there's nothing at this address.</p>
|
||||
</NotFound>
|
||||
</Router>
|
||||
```
|
||||
|
||||
## Common Blazor Pitfalls
|
||||
|
||||
### ❌ Avoid These Mistakes
|
||||
|
||||
1. **Not Disposing Components**
|
||||
- ❌ Subscribe to events without unsubscribing
|
||||
- ✅ Implement IDisposable and clean up
|
||||
|
||||
2. **Blocking UI Thread**
|
||||
- ❌ Using `Task.Result` or `.Wait()`
|
||||
- ✅ Always use `await`
|
||||
|
||||
3. **Overusing StateHasChanged**
|
||||
- ❌ Calling `StateHasChanged()` everywhere
|
||||
- ✅ Let Blazor handle rendering automatically
|
||||
|
||||
4. **Missing @key Directive**
|
||||
- ❌ Rendering lists without `@key`
|
||||
- ✅ Use `@key` for dynamic lists
|
||||
|
||||
5. **Not Handling Errors**
|
||||
- ❌ No error boundaries
|
||||
- ✅ Use ErrorBoundary component
|
||||
|
||||
## Blazor Checklist
|
||||
|
||||
### Component Development
|
||||
- [ ] Components are focused and reusable
|
||||
- [ ] Parameters use `[Parameter]` attribute
|
||||
- [ ] EventCallbacks for component events
|
||||
- [ ] Code-behind for complex logic
|
||||
- [ ] IDisposable implemented where needed
|
||||
|
||||
### Forms & Validation
|
||||
- [ ] EditForm used for forms
|
||||
- [ ] Data annotations validation
|
||||
- [ ] ValidationSummary displayed
|
||||
- [ ] Submit button disabled during submission
|
||||
- [ ] Error messages displayed
|
||||
|
||||
### State Management
|
||||
- [ ] Global state managed (Fluxor/FluxState)
|
||||
- [ ] Component state localized
|
||||
- [ ] Services injected via DI
|
||||
- [ ] State changes trigger re-renders
|
||||
|
||||
### API Integration
|
||||
- [ ] HttpClient configured
|
||||
- [ ] Loading states displayed
|
||||
- [ ] Error handling implemented
|
||||
- [ ] Authentication tokens included
|
||||
- [ ] Retry logic for transient failures
|
||||
|
||||
### Performance
|
||||
- [ ] Virtualization for long lists
|
||||
- [ ] Lazy loading for routes
|
||||
- [ ] @key directive on lists
|
||||
- [ ] Minimal JavaScript interop
|
||||
- [ ] Component disposal implemented
|
||||
|
||||
## Checklist Before Completion
|
||||
|
||||
- [ ] All components render correctly
|
||||
- [ ] Forms validate and submit
|
||||
- [ ] API calls successful
|
||||
- [ ] Loading states displayed
|
||||
- [ ] Error handling functional
|
||||
- [ ] Authentication working
|
||||
- [ ] State management functional
|
||||
- [ ] Performance optimized
|
||||
- [ ] Responsive design implemented
|
||||
- [ ] Documentation complete
|
||||
@@ -0,0 +1,163 @@
|
||||
---
|
||||
name: blazor
|
||||
description: |
|
||||
Builds Blazor WASM components for admin and main UI applications.
|
||||
Use when: Creating/modifying Razor components, configuring render modes, implementing authentication, managing component state, or working with MudBlazor components.
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash, mcp__context7__resolve-library-id, mcp__context7__query-docs
|
||||
---
|
||||
|
||||
# Blazor Skill
|
||||
|
||||
Sorcha uses Blazor with hybrid rendering (Server + WebAssembly). The Admin UI (`src/Apps/Sorcha.Admin/`) runs behind YARP API Gateway. Components use MudBlazor for UI and support three render modes: static server, interactive server, and interactive WASM.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Render Mode Selection
|
||||
|
||||
```razor
|
||||
@* WASM - Complex interactive pages (Designer, Diagrams) *@
|
||||
@page "/designer"
|
||||
@rendermode InteractiveWebAssembly
|
||||
@attribute [Authorize]
|
||||
|
||||
@* Server - Admin pages needing real-time SignalR *@
|
||||
@page "/admin/audit"
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
@attribute [Authorize(Roles = "Administrator")]
|
||||
|
||||
@* Static - Public pages (Login) - no @rendermode directive *@
|
||||
@page "/login"
|
||||
@attribute [AllowAnonymous]
|
||||
```
|
||||
|
||||
### Component with Loading State
|
||||
|
||||
```razor
|
||||
@inject HttpClient Http
|
||||
|
||||
<MudPaper Elevation="2" Class="pa-4">
|
||||
@if (_isLoading && !_hasLoadedOnce)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" Size="Size.Small" />
|
||||
}
|
||||
else if (_data != null)
|
||||
{
|
||||
<MudText>@_data.Title</MudText>
|
||||
}
|
||||
else if (_errorMessage != null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@_errorMessage</MudAlert>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private DataDto? _data;
|
||||
private string? _errorMessage;
|
||||
private bool _isLoading;
|
||||
private bool _hasLoadedOnce;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
await LoadDataAsync();
|
||||
}
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_data = await Http.GetFromJsonAsync<DataDto>("/api/data");
|
||||
_hasLoadedOnce = true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
| Concept | Usage | Example |
|
||||
|---------|-------|---------|
|
||||
| Render Mode | Control where component runs | `@rendermode InteractiveWebAssembly` |
|
||||
| CascadingParameter | Receive parent state | `[CascadingParameter] MudBlazor.IDialogReference? MudDialog` |
|
||||
| OnAfterRenderAsync | Initialize after DOM ready | `if (firstRender) await LoadAsync();` |
|
||||
| StateHasChanged | Trigger re-render | Call after async state updates |
|
||||
| NavigationManager | Programmatic navigation | `Navigation.NavigateTo("/", forceLoad: true)` |
|
||||
|
||||
## Project Structure
|
||||
|
||||
| Project | Purpose | Render Mode |
|
||||
|---------|---------|-------------|
|
||||
| `Sorcha.Admin` | Server host, auth, API proxy | Server + prerender |
|
||||
| `Sorcha.Admin.Client` | WASM components | WebAssembly |
|
||||
| `Sorcha.UI.Core` | Shared components | Both |
|
||||
| `Sorcha.UI.Web` | Main UI server | Server |
|
||||
| `Sorcha.UI.Web.Client` | Main UI WASM | WebAssembly |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### MudBlazor Dialog
|
||||
|
||||
```razor
|
||||
<MudDialog DisableSidePadding="false">
|
||||
<DialogContent>
|
||||
<MudTextField @bind-Value="_value" Label="Input" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Submit">OK</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[CascadingParameter] MudBlazor.IDialogReference? MudDialog { get; set; }
|
||||
private string _value = "";
|
||||
|
||||
private void Cancel() => MudDialog?.Close();
|
||||
private void Submit() => MudDialog?.Close(DialogResult.Ok(_value));
|
||||
}
|
||||
```
|
||||
|
||||
### Opening Dialog from Parent
|
||||
|
||||
```csharp
|
||||
var dialog = await DialogService.ShowAsync<LoginDialog>("Login");
|
||||
var result = await dialog.Result;
|
||||
if (result is { Canceled: false })
|
||||
{
|
||||
// Handle success
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [patterns](references/patterns.md) - Component and authentication patterns
|
||||
- [workflows](references/workflows.md) - Development and deployment workflows
|
||||
|
||||
## Related Skills
|
||||
|
||||
- See the **aspire** skill for service discovery configuration
|
||||
- See the **signalr** skill for real-time notifications
|
||||
- See the **jwt** skill for authentication token handling
|
||||
- See the **yarp** skill for API Gateway configuration
|
||||
- See the **mudblazor** skill for component library details
|
||||
|
||||
## Documentation Resources
|
||||
|
||||
> Fetch latest Blazor/MudBlazor documentation with Context7.
|
||||
|
||||
**Library ID:** `/websites/mudblazor` _(MudBlazor component library documentation)_
|
||||
|
||||
**Recommended Queries:**
|
||||
- "MudBlazor dialog service usage"
|
||||
- "MudBlazor form validation"
|
||||
- "MudBlazor data grid filtering"
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: |
|
||||
Styles Blazor WASM components with CSS and responsive design patterns using MudBlazor Material Design.
|
||||
Use when: Creating new components, styling existing components, implementing responsive layouts, adding animations/transitions, or working with the MudBlazor component library.
|
||||
allowed-tools: Read, Edit, Write, Glob, Grep, Bash
|
||||
---
|
||||
|
||||
# Frontend-design Skill
|
||||
|
||||
Sorcha uses **MudBlazor 9.5.0** for Material Design components with **CSS Isolation** as the primary styling approach. The design system follows Material Design 3 with custom extensions for blockchain/workflow visualization.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Styling a New Component
|
||||
|
||||
```razor
|
||||
@* MyComponent.razor *@
|
||||
<MudPaper Elevation="1" Class="pa-4 mb-3">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">Component Title</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
Description text
|
||||
</MudText>
|
||||
</MudPaper>
|
||||
```
|
||||
|
||||
### Custom Component with CSS Isolation
|
||||
|
||||
```razor
|
||||
@* MyCard.razor *@
|
||||
<div class="custom-card @(IsSelected ? "selected" : "")">
|
||||
<div class="card-header">@Title</div>
|
||||
<div class="card-content">@ChildContent</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public string Title { get; set; } = "";
|
||||
[Parameter] public RenderFragment? ChildContent { get; set; }
|
||||
[Parameter] public bool IsSelected { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
```css
|
||||
/* MyCard.razor.css */
|
||||
.custom-card {
|
||||
background: white;
|
||||
border: 2px solid #1976d2;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.custom-card:hover {
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.18);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.custom-card.selected {
|
||||
border-color: #0d47a1;
|
||||
border-width: 3px;
|
||||
}
|
||||
```
|
||||
|
||||
## Key Concepts
|
||||
|
||||
| Concept | Usage | Example |
|
||||
|---------|-------|---------|
|
||||
| CSS Isolation | Component-scoped styles | `Component.razor.css` |
|
||||
| MudBlazor Utility | Spacing, flex, alignment | `Class="d-flex pa-4 mb-3"` |
|
||||
| Color System | Semantic colors | `Color.Primary`, `Color.Error` |
|
||||
| Typography | Text hierarchy | `Typo.h6`, `Typo.body2` |
|
||||
| Elevation | Shadow depth | `Elevation="1"` (0-24) |
|
||||
| Breakpoint | Responsive | `Breakpoint.Sm` (641px) |
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Flex Layout with Gap
|
||||
|
||||
```razor
|
||||
<div class="d-flex align-center gap-2 mb-3">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Settings" />
|
||||
<MudText Typo="Typo.body1">Settings</MudText>
|
||||
<MudSpacer />
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Info">Active</MudChip>
|
||||
</div>
|
||||
```
|
||||
|
||||
### Panel with Header/Content Pattern
|
||||
|
||||
```razor
|
||||
<MudPaper Elevation="1" Class="panel">
|
||||
<div class="panel-header">
|
||||
<MudText Typo="Typo.subtitle1">Panel Title</MudText>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
@* Content here *@
|
||||
</div>
|
||||
</MudPaper>
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [aesthetics](references/aesthetics.md) - Color system, typography, brand identity
|
||||
- [components](references/components.md) - MudBlazor component patterns
|
||||
- [layouts](references/layouts.md) - Page structure, responsive grids
|
||||
- [motion](references/motion.md) - Transitions, hover effects
|
||||
- [patterns](references/patterns.md) - DO/DON'T design decisions
|
||||
|
||||
## Related Skills
|
||||
|
||||
- See the **blazor** skill for component lifecycle and state management
|
||||
- See the **signalr** skill for real-time UI updates
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Copilot Setup Steps
|
||||
|
||||
on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Configure private NuGet source credentials
|
||||
# NuGet.Config points to the private Integry feed; CI secrets provide auth.
|
||||
# Set NUGET_USERNAME and NUGET_PASSWORD in repository secrets.
|
||||
run: |
|
||||
dotnet nuget update source integry \
|
||||
--username "${{ secrets.NUGET_USERNAME }}" \
|
||||
--password "${{ secrets.NUGET_PASSWORD }}" \
|
||||
--store-password-in-clear-text
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore Fixiy.sln
|
||||
|
||||
- name: Build Fixiy.Web
|
||||
# MAUI builds require platform-specific workloads not available on ubuntu-latest.
|
||||
# Fixiy.Web covers the shared Blazor component tree and validates the full build.
|
||||
run: dotnet build Fixiy.Web/Fixiy.Web.csproj --no-restore -c Debug
|
||||
@@ -0,0 +1,15 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Rider ignored files
|
||||
/.idea.Fixiy.iml
|
||||
/projectSettingsUpdater.xml
|
||||
/modules.xml
|
||||
/contentModel.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="RiderAndroidProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="Fixiy.Maui">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" addBOMForNewFiles="with BOM under Windows, with no BOM otherwise" />
|
||||
</project>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.annotation.annotation">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.annotation/1.9.1.5/jar/androidx.annotation.annotation.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.annotation.annotation-jvm">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.annotation.jvm/1.9.1.5/jar/androidx.annotation.annotation-jvm.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.arch.core.core-common">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.arch.core.common/2.2.0.18/jar/androidx.arch.core.core-common.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.collection.collection">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.collection/1.5.0.3/jar/androidx.collection.collection.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.collection.collection-jvm">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.collection.jvm/1.5.0.3/jar/androidx.collection.collection-jvm.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.collection.collection-ktx">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.collection.ktx/1.5.0.3/jar/androidx.collection.collection-ktx.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.concurrent.concurrent-futures">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.concurrent.futures/1.3.0.1/jar/androidx.concurrent.concurrent-futures.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.constraintlayout.constraintlayout-core">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.constraintlayout.core/1.1.1.3/jar/androidx.constraintlayout.constraintlayout-core.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.lifecycle.lifecycle-common">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.lifecycle.common/2.9.2.1/jar/androidx.lifecycle.lifecycle-common.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.lifecycle.lifecycle-common-jvm">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.lifecycle.common.jvm/2.9.2.1/jar/androidx.lifecycle.lifecycle-common-jvm.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.resourceinspection.resourceinspection-annotation">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.resourceinspection.annotation/1.0.1.22/jar/androidx.resourceinspection.resourceinspection-annotation.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.window.window-core">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.window.windowcore/1.4.0.1/jar/androidx.window.window-core.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="androidx.window.window-core-jvm">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.androidx.window.windowcore.jvm/1.4.0.1/jar/androidx.window.window-core-jvm.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,9 @@
|
||||
<component name="libraryTable">
|
||||
<library name="org.jetbrains.kotlin.kotlin-stdlib-common-1.9.0">
|
||||
<CLASSES>
|
||||
<root url="jar://$USER_HOME$/.nuget/packages/xamarin.kotlin.stdlib.common/1.9.0.1/jar/org.jetbrains.kotlin.kotlin-stdlib-common-1.9.0.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,132 @@
|
||||
# Fixiy — Agent Guide
|
||||
|
||||
## Project Overview
|
||||
|
||||
Fixiy is a **.NET MAUI Blazor Hybrid** app that shares its entire UI with a **Blazor Server Web App** via a Razor Class Library (RCL). All pages and components live in `Fixiy.Shared` and are consumed by both host projects without modification.
|
||||
|
||||
- **App ID:** `it.integry.fixiy`
|
||||
- **Solution:** `Fixiy.sln`
|
||||
- **SDK:** .NET 10 (`global.json` — `rollForward: latestMajor`)
|
||||
- **NuGet source:** Private feed — see `NuGet.Config` (`https://nuget.studioml.it/repository/nuget-group/index.json`)
|
||||
- **Platforms:** Android 26+, iOS 15+, Web (Blazor Server)
|
||||
|
||||
## Repository Structure
|
||||
|
||||
```
|
||||
Fixiy.sln
|
||||
├── Fixiy.Maui/ # .NET MAUI Blazor Hybrid host (Android + iOS)
|
||||
│ ├── MauiProgram.cs # DI root — registers MAUI services + IntegryApiClient
|
||||
│ ├── Services/FormFactor.cs # MAUI implementation of IFormFactor (uses DeviceInfo)
|
||||
│ ├── Platforms/ # Platform-specific entry points (Android, iOS, …)
|
||||
│ └── Resources/ # App icons, splash, fonts, images
|
||||
├── Fixiy.Shared/ # Razor Class Library — all shared UI
|
||||
│ ├── Components/
|
||||
│ │ ├── Layout/ # MainLayout.razor, NavBar.razor, NavMenu.razor
|
||||
│ │ ├── Pages/ # Routable pages (e.g., Home.razor)
|
||||
│ │ └── SingleElements/ # Reusable components (e.g., NoDataAvailable.razor)
|
||||
│ ├── Interfaces/IFormFactor.cs # Platform abstraction interface
|
||||
│ ├── InteractiveRenderSettings.cs # Shared render mode constants (nulled in MAUI)
|
||||
│ └── _Imports.razor # Global using directives for shared components
|
||||
└── Fixiy.Web/ # Blazor Server Web App host
|
||||
├── Program.cs # DI root — registers web services + IntegryApiClient
|
||||
└── Services/FormFactor.cs # Web implementation of IFormFactor
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Mobile host | .NET MAUI Blazor Hybrid — `net10.0-android`, `net10.0-ios` |
|
||||
| Web host | Blazor Server (ASP.NET Core, `net10.0`) |
|
||||
| Shared UI | Razor Class Library (`Fixiy.Shared`, `net10.0`) |
|
||||
| Client SDK | `IntegryApiClient` — MAUI / Core / Blazor variants (private NuGet) |
|
||||
| Fonts | OpenSans via MAUI font registration |
|
||||
|
||||
## Build & Run
|
||||
|
||||
**Prerequisites:** .NET SDK 10, MAUI workload (`dotnet workload install maui`) for mobile builds.
|
||||
|
||||
```bash
|
||||
# Restore (requires private NuGet credentials — see NuGet.Config)
|
||||
dotnet restore Fixiy.sln
|
||||
|
||||
# Run Blazor Web App (no platform SDK required)
|
||||
dotnet run --project Fixiy.Web/Fixiy.Web.csproj
|
||||
|
||||
# Build MAUI for Android (requires MAUI workload)
|
||||
dotnet build Fixiy.Maui/Fixiy.Maui.csproj -f net10.0-android
|
||||
|
||||
# Build MAUI for iOS (requires macOS + Xcode)
|
||||
dotnet build Fixiy.Maui/Fixiy.Maui.csproj -f net10.0-ios
|
||||
```
|
||||
|
||||
## Key Patterns and Conventions
|
||||
|
||||
### 1. InteractiveRenderSettings pattern
|
||||
|
||||
`Fixiy.Shared/InteractiveRenderSettings.cs` exposes `static IComponentRenderMode?` properties (`InteractiveServer`, `InteractiveAuto`, `InteractiveWebAssembly`). In `Fixiy.Web` these remain set to real render modes. In `Fixiy.Maui`, `MauiProgram.ConfigureBlazorHybridRenderModes()` sets all three to `null` — MAUI Blazor Hybrid does not support server or WASM render modes.
|
||||
|
||||
**Always use the property, not the constant, in shared components:**
|
||||
|
||||
```razor
|
||||
@* Wrong — hard-coded constant, crashes in MAUI *@
|
||||
@rendermode="RenderMode.InteractiveServer"
|
||||
|
||||
@* Correct — resolves to null in MAUI, InteractiveServer in Web *@
|
||||
@rendermode="@InteractiveServer"
|
||||
```
|
||||
|
||||
### 2. IFormFactor platform abstraction
|
||||
|
||||
`IFormFactor` (in `Fixiy.Shared/Interfaces/`) provides `GetFormFactor()` and `GetPlatform()`. Each host project provides its own implementation registered in DI. Inject `IFormFactor` in shared components; **never call `DeviceInfo` or other MAUI APIs from `Fixiy.Shared`**.
|
||||
|
||||
### 3. IntegryApiClient registration
|
||||
|
||||
Both DI roots register IntegryApiClient with the same `appToken` and `useLoginAzienda: true`.
|
||||
|
||||
- MAUI: `.UseIntegry(appToken, useLoginAzienda: true)` on `MauiAppBuilder`
|
||||
- Web: `builder.Services.UseIntegry(appToken, useLoginAzienda: true)`
|
||||
|
||||
The app token must match across both registrations. It is currently declared as a `const` in `MauiProgram.cs` and a `const string` in `Fixiy.Web/Program.cs`.
|
||||
|
||||
### 4. Adding a new page
|
||||
|
||||
1. Create `Fixiy.Shared/Components/Pages/YourPage.razor` with `@page "/your-route"`
|
||||
2. No extra registration required — `Routes.razor` discovers pages via assembly scanning
|
||||
3. Add a navigation entry to `Fixiy.Shared/Components/Layout/NavMenu.razor` if user-accessible
|
||||
|
||||
### 5. Adding a platform-specific service
|
||||
|
||||
1. Define the interface in `Fixiy.Shared/Interfaces/IYourService.cs`
|
||||
2. Implement in `Fixiy.Maui/Services/YourService.cs` (may use MAUI APIs)
|
||||
3. Implement in `Fixiy.Web/Services/YourService.cs` (BCL only)
|
||||
4. Register in `Fixiy.Maui/MauiProgram.cs` as `AddSingleton<IYourService, YourService>()`
|
||||
5. Register in `Fixiy.Web/Program.cs` as `AddScoped<IYourService, YourService>()`
|
||||
|
||||
## Maintenance Matrix
|
||||
|
||||
| When you change… | Also update… |
|
||||
|------------------|-------------|
|
||||
| `Fixiy.Shared/Interfaces/IFormFactor.cs` | `Fixiy.Maui/Services/FormFactor.cs` and `Fixiy.Web/Services/FormFactor.cs` |
|
||||
| Any interface in `Fixiy.Shared/Interfaces/` | Both platform `Services/` implementations + both DI roots |
|
||||
| `InteractiveRenderSettings.cs` (add/remove property) | `MauiProgram.ConfigureBlazorHybridRenderModes()` — must null all new properties |
|
||||
| `Fixiy.Shared/_Imports.razor` | Verify imports are valid in both MAUI and Web (no platform-only namespaces) |
|
||||
| IntegryApiClient package version | Update in all three `.csproj` files consistently |
|
||||
| App token | Update in both `Fixiy.Maui/MauiProgram.cs` and `Fixiy.Web/Program.cs` |
|
||||
| `Fixiy.Shared/Components/Layout/NavMenu.razor` | Review navigation works on both platforms |
|
||||
|
||||
## CI/CD
|
||||
|
||||
No CI pipeline is currently configured. The project uses a private NuGet feed (`NuGet.Config`). For CI builds:
|
||||
|
||||
- Set `NUGET_USERNAME` and `NUGET_PASSWORD` (or PAT) as CI secrets
|
||||
- Build only `Fixiy.Web` in standard CI; MAUI builds require platform-specific agents with MAUI workloads
|
||||
- See `.github/workflows/copilot-setup-steps.yml` for environment setup reference
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Render mode constants in shared code:** Use `@InteractiveServer` (property from `InteractiveRenderSettings`) never `RenderMode.InteractiveServer`. The MAUI host nulls these via `ConfigureBlazorHybridRenderModes()`.
|
||||
- **MAUI APIs in Fixiy.Shared:** `DeviceInfo`, `FileSystem`, `Connectivity`, and other `Microsoft.Maui.*` APIs are not available in the RCL. Define a new `IFormFactor`-style interface instead.
|
||||
- **Private NuGet feed:** All `dotnet restore` calls require credentials. In CI, add the source credentials via environment secrets — the `NuGet.Config` `<clear/>` directive removes all default sources.
|
||||
- **MAUI workload for builds:** `dotnet workload install maui` is required before building `Fixiy.Maui`. Standard CI runners (Ubuntu) don't have it by default.
|
||||
- **Service lifetimes:** MAUI registers platform services as `Singleton` (long-lived app); Web registers them as `Scoped` (per request). Interfaces must not assume a specific lifetime.
|
||||
|
Before Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 121 KiB |
@@ -0,0 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.0]
|
||||
|
||||
### Added
|
||||
|
||||
- .NET MAUI Blazor Hybrid host targeting Android 26+ and iOS 15+
|
||||
- Blazor Server Web App host sharing UI via `Fixiy.Shared` Razor Class Library
|
||||
- `IFormFactor` platform abstraction for device/platform detection
|
||||
- `InteractiveRenderSettings` pattern for sharing render modes between MAUI and Web
|
||||
- IntegryApiClient integration for both MAUI and Web entry points
|
||||
- `NoDataAvailable` reusable component
|
||||
- `NavBar` and `NavMenu` layout components
|
||||
@@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Template.Maui"
|
||||
x:Class="Template.Maui.App">
|
||||
xmlns:local="clr-namespace:Fixiy.Maui"
|
||||
x:Class="Fixiy.Maui.App">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
@@ -1,22 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>$(TargetFrameworks);net8.0-android</TargetFrameworks>
|
||||
<TargetFrameworks>$(TargetFrameworks);net8.0-ios</TargetFrameworks>
|
||||
<TargetFrameworks>$(TargetFrameworks);net8.0-maccatalyst</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net8.0-windows10.0.19041.0</TargetFrameworks>
|
||||
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
|
||||
<!-- <TargetFrameworks>$(TargetFrameworks);net8.0-tizen</TargetFrameworks> -->
|
||||
|
||||
<!-- Note for MacCatalyst:
|
||||
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
|
||||
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
|
||||
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
|
||||
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
|
||||
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
|
||||
<TargetFrameworks>$(TargetFrameworks);net10.0-android</TargetFrameworks>
|
||||
<TargetFrameworks>$(TargetFrameworks);net10.0-ios</TargetFrameworks>
|
||||
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>Template.Maui</RootNamespace>
|
||||
<RootNamespace>Fixiy.Maui</RootNamespace>
|
||||
<UseMaui>true</UseMaui>
|
||||
<SingleProject>true</SingleProject>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
@@ -24,22 +13,17 @@
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<!-- Display name -->
|
||||
<ApplicationTitle>Template.Maui</ApplicationTitle>
|
||||
<ApplicationTitle>Fixiy</ApplicationTitle>
|
||||
|
||||
<!-- App Identifier -->
|
||||
<ApplicationId>it.integry.template.maui</ApplicationId>
|
||||
<ApplicationId>it.integry.fixiy</ApplicationId>
|
||||
|
||||
<!-- Versions -->
|
||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||
<ApplicationVersion>1</ApplicationVersion>
|
||||
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">14.2</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">
|
||||
14.0
|
||||
</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
|
||||
<!--slower build, faster runtime in DEBUG-->
|
||||
<!-- <_MauiForceXamlCForDebug Condition="'$(Configuration)' == 'Debug'">true</_MauiForceXamlCForDebug> -->
|
||||
</PropertyGroup>
|
||||
@@ -47,7 +31,7 @@
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
|
||||
<DefineConstants>$(DefineConstants);PLATFORM</DefineConstants>
|
||||
<SupportedOSPlatformVersion>26.0</SupportedOSPlatformVersion>
|
||||
<TargetPlatformVersion>34</TargetPlatformVersion>
|
||||
<TargetPlatformVersion>36</TargetPlatformVersion>
|
||||
|
||||
<!--<EmbedAssembliesIntoApk Condition="'$(Configuration)' == 'Debug'">true</EmbedAssembliesIntoApk>
|
||||
<AndroidPackageFormats Condition="'$(Configuration)' == 'Release'">aab</AndroidPackageFormats>
|
||||
@@ -74,11 +58,11 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios' OR $([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">
|
||||
<SupportedOSPlatformVersion>14.2</SupportedOSPlatformVersion>
|
||||
<SupportedOSPlatformVersion>15.0</SupportedOSPlatformVersion>
|
||||
<DefineConstants>$(DefineConstants);APPLE;PLATFORM</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(TargetFramework)'=='net8.0-ios'">
|
||||
<PropertyGroup Condition="'$(TargetFramework)'=='net10.0-ios'">
|
||||
<CodesignKey>Apple Development: Massimo Fausto Morelli (6C2CUM53BT)</CodesignKey>
|
||||
<CodesignProvision>VS: WildCard Development</CodesignProvision>
|
||||
</PropertyGroup>
|
||||
@@ -112,15 +96,16 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="IntegryApiClient.MAUI" Version="1.0.2" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="8.0.91" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="8.0.91" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="8.0.91" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="8.0.0" />
|
||||
<PackageReference Include="IntegryApiClient.MAUI" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls" Version="10.0.70" />
|
||||
<PackageReference Include="Microsoft.Maui.Controls.Compatibility" Version="10.0.70" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="10.0.70" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.8" />
|
||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Template.Shared\Template.Shared.csproj" />
|
||||
<ProjectReference Include="..\Fixiy.Shared\Fixiy.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||
xmlns:local="clr-namespace:Template.Maui"
|
||||
xmlns:shared="clr-namespace:Template.Shared;assembly=Template.Shared"
|
||||
x:Class="Template.Maui.MainPage"
|
||||
xmlns:local="clr-namespace:Fixiy.Maui"
|
||||
xmlns:shared="clr-namespace:Fixiy.Shared;assembly=Fixiy.Shared"
|
||||
x:Class="Fixiy.Maui.MainPage"
|
||||
BackgroundColor="{DynamicResource PageBackgroundColor}">
|
||||
|
||||
<BlazorWebView x:Name="blazorWebView" HostPage="wwwroot/index.html">
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
public partial class MainPage : ContentPage
|
||||
{
|
||||
@@ -1,19 +1,16 @@
|
||||
using IntegryApiClient.MAUI;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Template.Maui.Services;
|
||||
using Template.Shared;
|
||||
using Template.Shared.Interfaces;
|
||||
using Fixiy.Maui.Services;
|
||||
using Fixiy.Shared;
|
||||
using Fixiy.Shared.Interfaces;
|
||||
using Fixiy.Shared.Services;
|
||||
using MudBlazor.Services;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
public static class MauiProgram
|
||||
{
|
||||
#if DEBUG
|
||||
private const string BaseRestServicesEndpoint = "https://devservices.studioml.it/ems-api/";
|
||||
//private const string BaseRestServicesEndpoint = "http://192.168.2.23:8080/ems-api/";
|
||||
#else
|
||||
private const string BaseRestServicesEndpoint = "https://services.studioml.it/ems-api/";
|
||||
#endif
|
||||
private const string AppToken = "3e7e7147-1391-48e7-86bd-b70e7418d40d";
|
||||
|
||||
public static MauiApp CreateMauiApp()
|
||||
{
|
||||
@@ -22,14 +19,13 @@ namespace Template.Maui
|
||||
var builder = MauiApp.CreateBuilder();
|
||||
builder
|
||||
.UseMauiApp<App>()
|
||||
.UseIntegry(BaseRestServicesEndpoint)
|
||||
.UseIntegry(appToken: AppToken, useLoginAzienda: true)
|
||||
.ConfigureFonts(fonts =>
|
||||
{
|
||||
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
|
||||
});
|
||||
|
||||
builder.Services.AddMauiBlazorWebView();
|
||||
builder.Services.AddBlazorBootstrap();
|
||||
|
||||
#if DEBUG
|
||||
builder.Services.AddBlazorWebViewDeveloperTools();
|
||||
@@ -37,6 +33,8 @@ namespace Template.Maui
|
||||
#endif
|
||||
|
||||
builder.Services.AddSingleton<IFormFactor, FormFactor>();
|
||||
builder.Services.AddSingleton<MockAttivitaService>();
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
return builder.Build();
|
||||
}
|
||||
@@ -2,7 +2,7 @@ using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
@@ -1,7 +1,7 @@
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
[Application]
|
||||
public class MainApplication : MauiApplication
|
||||
@@ -1,7 +1,7 @@
|
||||
using Foundation;
|
||||
using Template.Maui;
|
||||
using Fixiy.Maui;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
@@ -1,7 +1,7 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
@@ -2,7 +2,7 @@ using System;
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Hosting;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
internal class Program : MauiApplication
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="8" xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common" />
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Template.Maui.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<ui-application appid="maui-application-id-placeholder" exec="Fixiy.Maui.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<label>maui-application-title-placeholder</label>
|
||||
<icon>maui-appicon-placeholder</icon>
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
||||
@@ -1,8 +1,8 @@
|
||||
<maui:MauiWinUIApplication
|
||||
x:Class="Template.Maui.WinUI.App"
|
||||
x:Class="Fixiy.Maui.WinUI.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:maui="using:Microsoft.Maui"
|
||||
xmlns:local="using:Template.Maui.WinUI">
|
||||
xmlns:local="using:Fixiy.Maui.WinUI">
|
||||
|
||||
</maui:MauiWinUIApplication>
|
||||
@@ -3,7 +3,7 @@ using Microsoft.UI.Xaml;
|
||||
// To learn more about WinUI, the WinUI project structure,
|
||||
// and more about our project templates, see: http://aka.ms/winui-project-info.
|
||||
|
||||
namespace Template.Maui.WinUI
|
||||
namespace Fixiy.Maui.WinUI
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="Template.Maui.WinUI.app"/>
|
||||
<assemblyIdentity version="1.0.0.0" name="Fixiy.Maui.WinUI.app"/>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
@@ -1,6 +1,6 @@
|
||||
using Foundation;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
@@ -1,7 +1,7 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace Template.Maui
|
||||
namespace Fixiy.Maui
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
|
Before Width: | Height: | Size: 229 B After Width: | Height: | Size: 229 B |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,6 +1,6 @@
|
||||
using Template.Shared.Interfaces;
|
||||
using Fixiy.Shared.Interfaces;
|
||||
|
||||
namespace Template.Maui.Services;
|
||||
namespace Fixiy.Maui.Services;
|
||||
|
||||
public class FormFactor : IFormFactor
|
||||
{
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>Fixiy.Maui</title>
|
||||
<base href="/" />
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
|
||||
|
||||
|
||||
<link href="_content/Fixiy.Shared/css/bootstrap/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
|
||||
<link href="_content/Fixiy.Shared/css/bootstrap/bootstrap-icons.min.css" rel="stylesheet" />
|
||||
<link href="_content/Blazor.Bootstrap/blazor.bootstrap.css" rel="stylesheet" />
|
||||
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/remixicon/remixicon.css" />
|
||||
<link rel="stylesheet" href="_content/MudBlazor/MudBlazor.min.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/app.css" />
|
||||
<link rel="stylesheet" href="_content/Fixiy.Shared/css/default-theme.css" />
|
||||
<link rel="stylesheet" href="Fixiy.Maui.styles.css" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="status-bar-safe-area"></div>
|
||||
|
||||
<div id="app">Loading...</div>
|
||||
|
||||
<div id="blazor-error-ui">
|
||||
An unhandled error has occurred.
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
|
||||
<script src="_framework/blazor.webview.js" autostart="false"></script>
|
||||
<script src="_content/Fixiy.Shared/js/signaturePad.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="_content/Fixiy.Shared/js/bootstrap/bootstrap.bundle.min.js" integrity="sha384-C6RzsynM9kWDrMnet107bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL" crossorigin="anonymous"></script>
|
||||
<!-- Add chart.js reference if chart components are used in your application. -->
|
||||
<!--<script src="_content/Fixiy.Shared/js/bootstrap/chart.umd.js" integrity="sha512-gQhCDsnnnUfaRzD8k1L5llCCV6O9HN09zClIzzeJ8OJ9MpGmIlCxm+pdCkqTwqJ4JcjbojFr79rl2F1mzcoLMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>-->
|
||||
<!-- Add chartjs-plugin-datalabels.min.js reference if chart components with data label feature is used in your application. -->
|
||||
<!--<script src="_content/Fixiy.Shared/js/bootstrap/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>-->
|
||||
<!-- Add sortable.js reference if SortableList component is used in your application. -->
|
||||
<!--<script src="_content/Fixiy.Shared/js/bootstrap/Sortable.min.js"></script>-->
|
||||
<script src="_content/Blazor.Bootstrap/blazor.bootstrap.js"></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -1,10 +1,11 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<div class="page">
|
||||
@*<div class="sidebar">
|
||||
<NavMenu />
|
||||
</div>*@
|
||||
<MudThemeProvider />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<div class="page">
|
||||
<NavMenu/>
|
||||
|
||||
<main>
|
||||
@@ -0,0 +1,9 @@
|
||||
.page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<nav class="bottom-nav">
|
||||
<NavLink class="nav-item" href="" Match="NavLinkMatch.All">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-home-5-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Home</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-item" href="scheda-viaggi" Match="NavLinkMatch.Prefix">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-route-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Viaggi</span>
|
||||
</NavLink>
|
||||
|
||||
<NavLink class="nav-item" href="attivita" Match="NavLinkMatch.Prefix">
|
||||
<div class="nav-icon-wrap">
|
||||
<i class="ri-task-line"></i>
|
||||
</div>
|
||||
<span class="nav-label">Attività</span>
|
||||
</NavLink>
|
||||
</nav>
|
||||
@@ -0,0 +1,92 @@
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
NavMenu — bottom nav, soft UI
|
||||
I .nav-item sono <a> resi da NavLink (componente figlio): vanno
|
||||
raggiunti con ::deep perché non ricevono l'attributo di scope.
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
.bottom-nav {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
bottom: calc(1rem + env(safe-area-inset-bottom, 0px));
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
height: 4.5rem;
|
||||
padding: 0 0.4rem;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(31, 30, 90, 0.05);
|
||||
border-radius: 26px;
|
||||
box-shadow: 0 12px 34px rgba(31, 30, 90, 0.14), 0 2px 8px rgba(31, 30, 90, 0.06);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 1;
|
||||
gap: 0.28rem;
|
||||
padding: 0.4rem 0;
|
||||
color: #9a9aae;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s ease;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item:hover {
|
||||
color: #6b6b7b;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item.active {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 3.2rem;
|
||||
height: 2.3rem;
|
||||
border-radius: 100px;
|
||||
background: transparent;
|
||||
transition: background 0.22s ease, transform 0.22s ease;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item.active .nav-icon-wrap {
|
||||
background: rgba(83, 82, 237, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item:hover:not(.active) .nav-icon-wrap {
|
||||
background: rgba(31, 30, 90, 0.05);
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item i {
|
||||
font-size: 1.45rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.22s ease;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item.active i {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-label {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bottom-nav ::deep .nav-item.active .nav-label {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
@page "/attivita"
|
||||
@rendermode @(InteractiveServer)
|
||||
@inject MockAttivitaService MockService
|
||||
|
||||
<div class="attivita-page">
|
||||
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Attività</h1>
|
||||
<span class="page-date">@DateTime.Today.ToString("dddd d MMMM", new System.Globalization.CultureInfo("it-IT"))</span>
|
||||
</div>
|
||||
<div class="page-header-actions">
|
||||
<span class="counter-badge">@_attivita.Count(a => a.Stato == StatoAttivita.Aperta) aperte</span>
|
||||
<button type="button" class="btn-fine-viaggio" @onclick="FineViaggio">
|
||||
<i class="ri-flag-2-line"></i>
|
||||
Fine Viaggio
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!_attivita.Any())
|
||||
{
|
||||
<NoDataAvailable
|
||||
ImageSource="_content/Fixiy.Shared/images/empty-state.svg"
|
||||
Text="Nessuna attività per oggi" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="attivita-list">
|
||||
@foreach (var item in AttivitaOrdinata)
|
||||
{
|
||||
<AttivitaCard
|
||||
Attivita="item"
|
||||
OnChiudi="ApriChiusura"
|
||||
OnVisualizzaAllegati="ApriAllegati"
|
||||
OnDragStart="OnDragStart"
|
||||
OnDrop="OnDrop" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<ChiusuraModal
|
||||
Attivita="_attivitaSelezionata"
|
||||
Visible="_chiusuraVisible"
|
||||
OnClosed="() => _chiusuraVisible = false"
|
||||
OnConferma="OnConfermaChiusura" />
|
||||
|
||||
<AllegatiModal
|
||||
Attivita="_attivitaSelezionata"
|
||||
Visible="_allegatiVisible"
|
||||
OnClosed="() => _allegatiVisible = false" />
|
||||
|
||||
@code {
|
||||
List<AttivitaItem> _attivita = [];
|
||||
AttivitaItem? _attivitaSelezionata;
|
||||
bool _chiusuraVisible;
|
||||
bool _allegatiVisible;
|
||||
AttivitaItem? _dragSource;
|
||||
|
||||
IEnumerable<AttivitaItem> AttivitaOrdinata =>
|
||||
_attivita.OrderBy(a => a.Priorita switch
|
||||
{
|
||||
PrioritaAttivita.Emergenza => 0,
|
||||
PrioritaAttivita.Alta => 1,
|
||||
_ => 2
|
||||
})
|
||||
.ThenBy(a => a.Ordine);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_attivita = MockService.GetAttivitaOggi();
|
||||
}
|
||||
|
||||
void ApriChiusura(AttivitaItem item)
|
||||
{
|
||||
_attivitaSelezionata = item;
|
||||
_chiusuraVisible = true;
|
||||
}
|
||||
|
||||
void ApriAllegati(AttivitaItem item)
|
||||
{
|
||||
_attivitaSelezionata = item;
|
||||
_allegatiVisible = true;
|
||||
}
|
||||
|
||||
void OnConfermaChiusura((AttivitaItem item, StatoAttivita stato) args)
|
||||
{
|
||||
var idx = _attivita.IndexOf(args.item);
|
||||
if (idx >= 0)
|
||||
_attivita[idx] = args.item with { Stato = args.stato };
|
||||
|
||||
_chiusuraVisible = false;
|
||||
}
|
||||
|
||||
void OnDragStart(AttivitaItem item)
|
||||
{
|
||||
if (!item.IsLocked)
|
||||
_dragSource = item;
|
||||
}
|
||||
|
||||
void OnDrop(AttivitaItem target)
|
||||
{
|
||||
if (_dragSource is null || _dragSource == target || target.IsLocked)
|
||||
return;
|
||||
|
||||
var srcIdx = _attivita.IndexOf(_dragSource);
|
||||
var tgtIdx = _attivita.IndexOf(target);
|
||||
|
||||
if (srcIdx < 0 || tgtIdx < 0) return;
|
||||
|
||||
_attivita.RemoveAt(srcIdx);
|
||||
_attivita.Insert(tgtIdx, _dragSource);
|
||||
|
||||
for (int i = 0; i < _attivita.Count; i++)
|
||||
_attivita[i] = _attivita[i] with { Ordine = i };
|
||||
|
||||
_dragSource = null;
|
||||
}
|
||||
|
||||
void FineViaggio()
|
||||
{
|
||||
// TODO: generare e inviare PDF/CSV via email
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
.attivita-page {
|
||||
padding: 0;
|
||||
padding-top: calc(0.75rem + env(safe-area-inset-top, 0px));
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-date {
|
||||
font-size: 0.78rem;
|
||||
color: #999;
|
||||
text-transform: capitalize;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
.counter-badge {
|
||||
background: var(--primary-color);
|
||||
color: #fff;
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
padding: 0.3rem 0.85rem;
|
||||
border-radius: 100px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 4px 12px rgba(83, 82, 237, 0.28);
|
||||
}
|
||||
|
||||
.attivita-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
/* spazio per la bottom nav */
|
||||
padding-bottom: 7rem;
|
||||
}
|
||||
|
||||
/* Bottone "Fine Viaggio" integrato nell'header */
|
||||
.btn-fine-viaggio {
|
||||
background: linear-gradient(135deg, #5352ed, #3f3bc4);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 100px;
|
||||
padding: 0.5rem 1.1rem;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.01em;
|
||||
white-space: nowrap;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
box-shadow: 0 5px 16px rgba(83, 82, 237, 0.32);
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.18s ease, transform 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn-fine-viaggio i {
|
||||
font-size: 1.05rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.btn-fine-viaggio:hover {
|
||||
box-shadow: 0 7px 20px rgba(83, 82, 237, 0.42);
|
||||
}
|
||||
|
||||
.btn-fine-viaggio:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
@page "/"
|
||||
|
||||
<h1>Hello, world!</h1>
|
||||
@@ -0,0 +1,11 @@
|
||||
@page "/scheda-viaggi"
|
||||
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">Scheda Viaggi</h1>
|
||||
</div>
|
||||
|
||||
<div class="page-content">
|
||||
<NoDataAvailable
|
||||
ImageSource="_content/Fixiy.Shared/images/empty-state.svg"
|
||||
Text="Nessun viaggio disponibile" />
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<MudDialog Visible="@(Attivita is not null && Visible)" VisibleChanged="OnVisibleChanged"
|
||||
Options="BottomSheetOptions" Class="bottom-sheet">
|
||||
<TitleContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="sheet-grabber"></div>
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-title">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Attachment" Size="Size.Small" />
|
||||
Allegati — @Attivita.PuntoVendita
|
||||
</span>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
|
||||
Class="sheet-close" OnClick="Chiudi" />
|
||||
</div>
|
||||
}
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="allegati-list">
|
||||
@foreach (var allegato in Attivita.Allegati)
|
||||
{
|
||||
<div class="allegato-item">
|
||||
@if (allegato.Tipo is TipoAllegato.Immagine or TipoAllegato.Piantina)
|
||||
{
|
||||
<div class="allegato-preview">
|
||||
<img src="@allegato.Url" alt="@allegato.Nome" loading="lazy" />
|
||||
<span class="allegato-type-badge @(allegato.Tipo == TipoAllegato.Piantina ? "badge-piantina" : "badge-foto")">
|
||||
@(allegato.Tipo == TipoAllegato.Piantina ? "Piantina" : "Foto")
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="allegato-icon-box @(allegato.Tipo == TipoAllegato.Email ? "icon-email" : "icon-doc")">
|
||||
<MudIcon Icon="@(allegato.Tipo == TipoAllegato.Email ? Icons.Material.Outlined.Email : Icons.Material.Outlined.Article)"
|
||||
Size="Size.Large" />
|
||||
</div>
|
||||
}
|
||||
<div class="allegato-info">
|
||||
<span class="allegato-nome">@allegato.Nome</span>
|
||||
<a href="@allegato.Url" class="allegato-download" target="_blank" title="Apri allegato">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.OpenInNew" Size="Size.Small" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[Parameter] public AttivitaItem? Attivita { get; set; }
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
[Parameter] public EventCallback OnClosed { get; set; }
|
||||
|
||||
static readonly DialogOptions BottomSheetOptions = new()
|
||||
{
|
||||
Position = DialogPosition.BottomCenter,
|
||||
FullWidth = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
CloseButton = false,
|
||||
BackdropClick = true,
|
||||
CloseOnEscapeKey = true
|
||||
};
|
||||
|
||||
Task Chiudi() => OnClosed.InvokeAsync();
|
||||
|
||||
async Task OnVisibleChanged(bool visible)
|
||||
{
|
||||
if (!visible)
|
||||
await Chiudi();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/* Allegati list */
|
||||
.allegati-list {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 0.8rem 1rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.allegato-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.allegato-preview {
|
||||
position: relative;
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.allegato-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.allegato-type-badge {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 100px;
|
||||
}
|
||||
|
||||
.badge-foto { background: rgba(83, 82, 237, 0.85); color: #fff; }
|
||||
.badge-piantina { background: rgba(0, 150, 136, 0.85); color: #fff; }
|
||||
|
||||
.allegato-icon-box {
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.icon-email { background: #e3f2fd; color: #1565c0; }
|
||||
.icon-doc { background: #e8f5e9; color: #2e7d32; }
|
||||
|
||||
.allegato-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.allegato-nome {
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 80%;
|
||||
}
|
||||
|
||||
.allegato-download {
|
||||
font-size: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<article class="@CardClass"
|
||||
draggable="@(IsDraggable ? "true" : "false")"
|
||||
@ondragstart="@(() => OnDragStart.InvokeAsync(Attivita))"
|
||||
@ondragover:preventDefault
|
||||
@ondrop="@(() => OnDrop.InvokeAsync(Attivita))">
|
||||
|
||||
<span class="accent-bar"></span>
|
||||
|
||||
<div class="card-body">
|
||||
|
||||
<header class="card-top">
|
||||
<span class="@($"priorita-pill {PrioritaClass}")">
|
||||
<MudIcon Icon="@PrioritaIcon" Size="Size.Small" />
|
||||
<span>@Attivita.Priorita.ToString().ToUpper()</span>
|
||||
</span>
|
||||
|
||||
@if (Attivita.IsLocked)
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small"
|
||||
Class="handle-icon lock-icon" Title="Emergenza: non riordinabile" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudIcon Icon="@Icons.Material.Filled.DragIndicator" Size="Size.Small"
|
||||
Class="handle-icon drag-icon" Title="Trascina per riordinare" />
|
||||
}
|
||||
</header>
|
||||
|
||||
<h3 class="card-store">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Store" Size="Size.Small" />
|
||||
<span>@Attivita.PuntoVendita</span>
|
||||
</h3>
|
||||
|
||||
<nav class="card-breadcrumb">
|
||||
<span>@Attivita.Categoria</span>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Size="Size.Small" />
|
||||
<span>@Attivita.Sottocategoria</span>
|
||||
<MudIcon Icon="@Icons.Material.Filled.ChevronRight" Size="Size.Small" />
|
||||
<span>@Attivita.Reparto</span>
|
||||
</nav>
|
||||
|
||||
<div class="card-location">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.LocationOn" Size="Size.Small" />
|
||||
<span>@Attivita.Luogo</span>
|
||||
</div>
|
||||
|
||||
<p class="card-desc">@Attivita.Descrizione</p>
|
||||
|
||||
@if (Attivita.Allegati.Count > 0)
|
||||
{
|
||||
<span class="card-allegati-pill">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.AttachFile" Size="Size.Small" />
|
||||
<span>@Attivita.Allegati.Count allegat@(Attivita.Allegati.Count == 1 ? "o" : "i")</span>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<footer class="card-actions">
|
||||
@if (Attivita.Allegati.Count > 0)
|
||||
{
|
||||
<button type="button" class="btn-card btn-card-ghost"
|
||||
@onclick="@(() => OnVisualizzaAllegati.InvokeAsync(Attivita))">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.FolderOpen" Size="Size.Small" />
|
||||
<span>Allegati</span>
|
||||
</button>
|
||||
}
|
||||
@if (Attivita.Stato == StatoAttivita.Aperta)
|
||||
{
|
||||
<button type="button" class="btn-card btn-card-primary"
|
||||
@onclick="@(() => OnChiudi.InvokeAsync(Attivita))">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" />
|
||||
<span>Chiudi Attività</span>
|
||||
</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="@($"stato-pill {StatoClass}")">
|
||||
<MudIcon Icon="@StatoIcon" Size="Size.Small" />
|
||||
<span>@Attivita.Stato</span>
|
||||
</span>
|
||||
}
|
||||
</footer>
|
||||
</article>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public AttivitaItem? Attivita { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnChiudi { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnVisualizzaAllegati { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnDragStart { get; set; }
|
||||
[Parameter] public EventCallback<AttivitaItem> OnDrop { get; set; }
|
||||
|
||||
bool IsDraggable => Attivita?.Priorita == PrioritaAttivita.Normale && Attivita.Stato == StatoAttivita.Aperta;
|
||||
|
||||
string CardClass => $"attivita-card {PrioritaClass}{(Attivita?.Stato != StatoAttivita.Aperta ? " card-chiusa" : "")}";
|
||||
|
||||
string PrioritaClass => Attivita?.Priorita switch
|
||||
{
|
||||
PrioritaAttivita.Emergenza => "priorita-emergenza",
|
||||
PrioritaAttivita.Alta => "priorita-alta",
|
||||
_ => "priorita-normale"
|
||||
};
|
||||
|
||||
string PrioritaIcon => Attivita?.Priorita switch
|
||||
{
|
||||
PrioritaAttivita.Emergenza => Icons.Material.Filled.Warning,
|
||||
PrioritaAttivita.Alta => Icons.Material.Filled.ErrorOutline,
|
||||
_ => Icons.Material.Filled.CheckCircleOutline
|
||||
};
|
||||
|
||||
string StatoClass => Attivita?.Stato switch
|
||||
{
|
||||
StatoAttivita.Chiusa => "stato-chiusa",
|
||||
StatoAttivita.Rimandata => "stato-rimandata",
|
||||
_ => "stato-aperta"
|
||||
};
|
||||
|
||||
string StatoIcon => Attivita?.Stato switch
|
||||
{
|
||||
StatoAttivita.Chiusa => Icons.Material.Filled.CheckCircleOutline,
|
||||
_ => Icons.Material.Filled.Schedule
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
/* ─────────────────────────────────────────────────────────────
|
||||
AttivitaCard — soft UI
|
||||
───────────────────────────────────────────────────────────── */
|
||||
|
||||
.attivita-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 20px;
|
||||
background: #ffffff;
|
||||
border: 1px solid rgba(31, 30, 90, 0.05);
|
||||
box-shadow: 0 6px 22px rgba(31, 30, 90, 0.07), 0 1px 3px rgba(31, 30, 90, 0.04);
|
||||
overflow: hidden;
|
||||
transition: transform 0.22s ease, box-shadow 0.22s ease, opacity 0.22s ease;
|
||||
}
|
||||
|
||||
.attivita-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 14px 34px rgba(31, 30, 90, 0.13), 0 2px 6px rgba(31, 30, 90, 0.06);
|
||||
}
|
||||
|
||||
/* Accent bar — priority */
|
||||
.accent-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 5px;
|
||||
border-radius: 20px 0 0 20px;
|
||||
}
|
||||
|
||||
.attivita-card.priorita-emergenza .accent-bar { background: linear-gradient(180deg, #ff6b6b, #d32f2f); }
|
||||
.attivita-card.priorita-alta .accent-bar { background: linear-gradient(180deg, #ffb74d, #ef6c00); }
|
||||
.attivita-card.priorita-normale .accent-bar { background: linear-gradient(180deg, #8c8bff, var(--primary-color)); }
|
||||
|
||||
/* Closed / non-open cards */
|
||||
.attivita-card.card-chiusa {
|
||||
opacity: 0.62;
|
||||
box-shadow: 0 2px 10px rgba(31, 30, 90, 0.05);
|
||||
}
|
||||
|
||||
.attivita-card.card-chiusa:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 2px 10px rgba(31, 30, 90, 0.05);
|
||||
}
|
||||
|
||||
/* Drag affordance */
|
||||
.attivita-card[draggable="true"] { cursor: grab; }
|
||||
.attivita-card[draggable="true"]:active { cursor: grabbing; }
|
||||
|
||||
/* ─── Body ─── */
|
||||
|
||||
.card-body {
|
||||
padding: 1rem 1.1rem 0.4rem 1.25rem;
|
||||
}
|
||||
|
||||
.card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.priorita-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
height: 26px;
|
||||
padding: 0 0.7rem;
|
||||
border-radius: 100px;
|
||||
font-size: 0.66rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.priorita-pill ::deep .mud-icon-root { font-size: 0.95rem; }
|
||||
|
||||
.priorita-pill.priorita-emergenza { background: #ffe9e9; color: #c62828; }
|
||||
.priorita-pill.priorita-alta { background: #fff2df; color: #e65100; }
|
||||
.priorita-pill.priorita-normale { background: #ecebff; color: #3f3bc4; }
|
||||
|
||||
.card-top ::deep .handle-icon { font-size: 1.2rem; }
|
||||
.card-top ::deep .lock-icon { color: #e53935; opacity: 0.75; }
|
||||
.card-top ::deep .drag-icon { color: #c4c4d4; }
|
||||
|
||||
.card-store {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
margin: 0 0 0.4rem;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
color: var(--darker-color);
|
||||
}
|
||||
|
||||
.card-store ::deep .mud-icon-root { color: var(--primary-color); }
|
||||
|
||||
.card-breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.15rem;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.72rem;
|
||||
color: #9a9aae;
|
||||
}
|
||||
|
||||
.card-breadcrumb ::deep .mud-icon-root { font-size: 0.9rem; color: #c7c7d6; }
|
||||
|
||||
.card-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
margin-bottom: 0.55rem;
|
||||
font-size: 0.76rem;
|
||||
color: #6b6b7b;
|
||||
}
|
||||
|
||||
.card-location ::deep .mud-icon-root { font-size: 1rem; color: #9a9aae; }
|
||||
|
||||
.card-desc {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
margin: 0 0 0.6rem;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
color: #4a4a57;
|
||||
}
|
||||
|
||||
.card-allegati-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
border-radius: 100px;
|
||||
background: rgba(83, 82, 237, 0.08);
|
||||
color: var(--primary-color);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.card-allegati-pill ::deep .mud-icon-root { font-size: 0.95rem; }
|
||||
|
||||
/* ─── Actions ─── */
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 1.1rem 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.btn-card {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
height: 36px;
|
||||
padding: 0 1rem;
|
||||
border-radius: 100px;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.btn-card ::deep .mud-icon-root { font-size: 1.05rem; }
|
||||
|
||||
.btn-card:active { transform: scale(0.96); }
|
||||
|
||||
.btn-card-ghost {
|
||||
background: #f3f3fb;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.btn-card-ghost:hover { background: #e9e9fb; }
|
||||
|
||||
.btn-card-primary {
|
||||
background: linear-gradient(135deg, #5352ed, #3f3bc4);
|
||||
color: #fff;
|
||||
box-shadow: 0 5px 16px rgba(83, 82, 237, 0.32);
|
||||
}
|
||||
|
||||
.btn-card-primary:hover { box-shadow: 0 7px 20px rgba(83, 82, 237, 0.42); }
|
||||
|
||||
/* Stato pill (non-open cards) */
|
||||
.stato-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
height: 30px;
|
||||
padding: 0 0.85rem;
|
||||
border-radius: 100px;
|
||||
font-size: 0.74rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stato-pill ::deep .mud-icon-root { font-size: 1rem; }
|
||||
|
||||
.stato-pill.stato-chiusa { background: #e6f6ec; color: #2e7d32; }
|
||||
.stato-pill.stato-rimandata { background: #fff4e0; color: #e08600; }
|
||||
.stato-pill.stato-aperta { background: #ecebff; color: #3f3bc4; }
|
||||
@@ -0,0 +1,259 @@
|
||||
<MudDialog Visible="@(Attivita is not null && Visible)" VisibleChanged="OnVisibleChanged"
|
||||
Options="BottomSheetOptions" Class="bottom-sheet">
|
||||
<TitleContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="sheet-grabber"></div>
|
||||
<div class="sheet-header">
|
||||
<span class="sheet-title">
|
||||
<MudIcon Icon="@Icons.Material.Outlined.Build" Size="Size.Small" />
|
||||
@Attivita.PuntoVendita
|
||||
</span>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Close" Size="Size.Small"
|
||||
Class="sheet-close" OnClick="Annulla" />
|
||||
</div>
|
||||
}
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
@if (Attivita is not null)
|
||||
{
|
||||
<div class="tab-bar">
|
||||
<button class="tab-btn @(_tab == "chiudi" ? "active" : "")" @onclick='() => _tab = "chiudi"'>
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Small" /> Chiudi
|
||||
</button>
|
||||
<button class="tab-btn @(_tab == "rimanda" ? "active" : "")" @onclick='() => _tab = "rimanda"'>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" /> Rimanda
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="sheet-body">
|
||||
|
||||
@if (_tab == "chiudi")
|
||||
{
|
||||
<div class="form-row">
|
||||
<MudTimePicker Label="Ora ingresso" @bind-Time="_oraIngresso"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
<MudTimePicker Label="Ora fine lavori" @bind-Time="_oraFine"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">Operatori</MudText>
|
||||
<div class="operatori-list">
|
||||
@for (int i = 0; i < _operatori.Count; i++)
|
||||
{
|
||||
int idx = i;
|
||||
<div class="operatore-row">
|
||||
<MudTextField T="string" Value="_operatori[idx]"
|
||||
ValueChanged="v => _operatori[idx] = v"
|
||||
Placeholder="Nome operatore"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
@if (_operatori.Count > 1)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Remove"
|
||||
Color="Color.Error" Size="Size.Small"
|
||||
OnClick="() => _operatori.RemoveAt(idx)" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<MudButton StartIcon="@Icons.Material.Filled.Add" Variant="Variant.Text"
|
||||
Color="Color.Primary" Size="Size.Small"
|
||||
OnClick="() => _operatori.Add(string.Empty)">
|
||||
Aggiungi operatore
|
||||
</MudButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<MudTextField T="string" Label="Descrizione intervento"
|
||||
@bind-Value="_descrizioneIntervento"
|
||||
Lines="3" Placeholder="Descrivi l intervento eseguito..."
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Required="true"
|
||||
Error="@(_validato && string.IsNullOrWhiteSpace(_descrizioneIntervento))"
|
||||
ErrorText="Campo obbligatorio" />
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">
|
||||
Firma Capo Negozio <span class="required">*</span>
|
||||
</MudText>
|
||||
<SignaturePad OnFirmaCambiata="f => _firma = f" />
|
||||
@if (_validato && string.IsNullOrWhiteSpace(_firma))
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">Firma obbligatoria</MudText>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">Foto lavoro svolto</MudText>
|
||||
<InputFile class="form-input-file" accept="image/*" multiple
|
||||
OnChange="OnFotoChiusura" />
|
||||
@if (_fotoNomi.Any())
|
||||
{
|
||||
<div class="foto-preview-list">
|
||||
@foreach (var nome in _fotoNomi)
|
||||
{
|
||||
<MudChip T="string" Icon="@Icons.Material.Outlined.Image"
|
||||
Size="Size.Small" Color="Color.Primary">@nome</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<MudTextField T="string" Label="Commento foto (opzionale)"
|
||||
@bind-Value="_commentoFoto" Lines="2"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Class="mt-2" />
|
||||
</div>
|
||||
|
||||
<MudButton FullWidth="true" Variant="Variant.Filled" Color="Color.Primary"
|
||||
Size="Size.Large" StartIcon="@Icons.Material.Filled.CheckCircleOutline"
|
||||
OnClick="ConfermaChiusura">
|
||||
Chiudi Attivita
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudTextField T="string" Label="Motivo del rimando"
|
||||
@bind-Value="_motivoRimando" Lines="3"
|
||||
Placeholder="Es. Non di competenza ufficio tecnico..."
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense"
|
||||
Required="true"
|
||||
Error="@(_validato && string.IsNullOrWhiteSpace(_motivoRimando))"
|
||||
ErrorText="Campo obbligatorio" />
|
||||
|
||||
<MudSelect T="string" Label="Azienda terza (se di competenza)"
|
||||
@bind-Value="_aziendaTerza"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense">
|
||||
<MudSelectItem T="string" Value='""'>Nessuna</MudSelectItem>
|
||||
@foreach (var az in _aziendeTerme)
|
||||
{
|
||||
<MudSelectItem T="string" Value="@az">@az</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
|
||||
@if (_aziendaTerza == "Altro")
|
||||
{
|
||||
<MudTextField T="string" Label="Specifica azienda"
|
||||
@bind-Value="_aziendaAltra"
|
||||
Variant="Variant.Outlined" Margin="Margin.Dense" />
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<MudText Typo="Typo.caption" Class="form-label">
|
||||
Foto KO <span class="required">*</span>
|
||||
</MudText>
|
||||
<InputFile class="form-input-file" accept="image/*" multiple
|
||||
OnChange="OnFotoRimando" />
|
||||
@if (_fotoRimandoNomi.Any())
|
||||
{
|
||||
<div class="foto-preview-list">
|
||||
@foreach (var nome in _fotoRimandoNomi)
|
||||
{
|
||||
<MudChip T="string" Icon="@Icons.Material.Outlined.Image"
|
||||
Size="Size.Small" Color="Color.Warning">@nome</MudChip>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (_validato && !_fotoRimandoNomi.Any())
|
||||
{
|
||||
<MudText Typo="Typo.caption" Color="Color.Error">Almeno una foto e obbligatoria</MudText>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudButton FullWidth="true" Variant="Variant.Filled" Color="Color.Warning"
|
||||
Size="Size.Large" StartIcon="@Icons.Material.Filled.ArrowForward"
|
||||
OnClick="ConfermaRimando">
|
||||
Rimanda Attivita
|
||||
</MudButton>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</DialogContent>
|
||||
</MudDialog>
|
||||
|
||||
@code {
|
||||
[Parameter] public AttivitaItem? Attivita { get; set; }
|
||||
[Parameter] public bool Visible { get; set; }
|
||||
[Parameter] public EventCallback OnClosed { get; set; }
|
||||
[Parameter] public EventCallback<(AttivitaItem, StatoAttivita)> OnConferma { get; set; }
|
||||
|
||||
string _tab = "chiudi";
|
||||
bool _validato;
|
||||
|
||||
static readonly DialogOptions BottomSheetOptions = new()
|
||||
{
|
||||
Position = DialogPosition.BottomCenter,
|
||||
FullWidth = true,
|
||||
MaxWidth = MaxWidth.Medium,
|
||||
CloseButton = false,
|
||||
BackdropClick = true,
|
||||
CloseOnEscapeKey = true
|
||||
};
|
||||
|
||||
async Task OnVisibleChanged(bool visible)
|
||||
{
|
||||
if (!visible)
|
||||
await Annulla();
|
||||
}
|
||||
|
||||
TimeSpan? _oraIngresso = DateTime.Now.TimeOfDay;
|
||||
TimeSpan? _oraFine = DateTime.Now.TimeOfDay;
|
||||
|
||||
List<string> _operatori = ["Marco Esposito"];
|
||||
string _descrizioneIntervento = string.Empty;
|
||||
string _firma = string.Empty;
|
||||
string _commentoFoto = string.Empty;
|
||||
List<string> _fotoNomi = [];
|
||||
|
||||
string _motivoRimando = string.Empty;
|
||||
string _aziendaTerza = string.Empty;
|
||||
string _aziendaAltra = string.Empty;
|
||||
List<string> _fotoRimandoNomi = [];
|
||||
|
||||
readonly List<string> _aziendeTerme = ["Arneg", "Desich", "Idracol", "Carrier", "Danfoss", "Alfa Laval", "Altro"];
|
||||
|
||||
void OnFotoChiusura(InputFileChangeEventArgs e) =>
|
||||
_fotoNomi = e.GetMultipleFiles().Select(f => f.Name).ToList();
|
||||
|
||||
void OnFotoRimando(InputFileChangeEventArgs e) =>
|
||||
_fotoRimandoNomi = e.GetMultipleFiles().Select(f => f.Name).ToList();
|
||||
|
||||
async Task ConfermaChiusura()
|
||||
{
|
||||
_validato = true;
|
||||
if (string.IsNullOrWhiteSpace(_descrizioneIntervento) || string.IsNullOrWhiteSpace(_firma))
|
||||
return;
|
||||
if (Attivita is not null)
|
||||
await OnConferma.InvokeAsync((Attivita, StatoAttivita.Chiusa));
|
||||
Reset();
|
||||
}
|
||||
|
||||
async Task ConfermaRimando()
|
||||
{
|
||||
_validato = true;
|
||||
if (string.IsNullOrWhiteSpace(_motivoRimando) || !_fotoRimandoNomi.Any())
|
||||
return;
|
||||
if (Attivita is not null)
|
||||
await OnConferma.InvokeAsync((Attivita, StatoAttivita.Rimandata));
|
||||
Reset();
|
||||
}
|
||||
|
||||
Task Annulla()
|
||||
{
|
||||
Reset();
|
||||
return OnClosed.InvokeAsync();
|
||||
}
|
||||
|
||||
void Reset()
|
||||
{
|
||||
_validato = false;
|
||||
_tab = "chiudi";
|
||||
_descrizioneIntervento = string.Empty;
|
||||
_firma = string.Empty;
|
||||
_commentoFoto = string.Empty;
|
||||
_fotoNomi = [];
|
||||
_motivoRimando = string.Empty;
|
||||
_aziendaTerza = string.Empty;
|
||||
_aziendaAltra = string.Empty;
|
||||
_fotoRimandoNomi = [];
|
||||
_operatori = ["Marco Esposito"];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
padding: 0 1rem;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
padding: 0.7rem 0.3rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.3rem;
|
||||
transition: color 0.2s, border-color 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--primary-color);
|
||||
border-bottom-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 1rem 1.1rem 1.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.form-row .form-group {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-weight: 600 !important;
|
||||
color: var(--darker-color) !important;
|
||||
}
|
||||
|
||||
.required { color: #e53935; }
|
||||
|
||||
.operatori-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.operatore-row {
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form-input-file {
|
||||
font-size: 0.78rem;
|
||||
color: #666;
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
.foto-preview-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.mt-2 { margin-top: 0.5rem !important; }
|
||||
@@ -0,0 +1,43 @@
|
||||
@inject IJSRuntime JS
|
||||
@implements IAsyncDisposable
|
||||
|
||||
<div class="signature-container">
|
||||
<canvas id="@_canvasId" width="600" height="200" class="signature-canvas"></canvas>
|
||||
<div class="signature-actions">
|
||||
<button type="button" class="btn-clear-sig" @onclick="CancellaFirma">
|
||||
<i class="ri-delete-bin-line"></i>
|
||||
Cancella firma
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public EventCallback<string> OnFirmaCambiata { get; set; }
|
||||
|
||||
readonly string _canvasId = $"sig-{Guid.NewGuid():N}";
|
||||
DotNetObjectReference<SignaturePad>? _dotNetRef;
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
_dotNetRef = DotNetObjectReference.Create(this);
|
||||
await JS.InvokeVoidAsync("signaturePad.init", _canvasId, _dotNetRef);
|
||||
}
|
||||
}
|
||||
|
||||
[JSInvokable]
|
||||
public Task OnSignatureChanged(string dataUrl) => OnFirmaCambiata.InvokeAsync(dataUrl);
|
||||
|
||||
async Task CancellaFirma()
|
||||
{
|
||||
await JS.InvokeVoidAsync("signaturePad.clear", _canvasId);
|
||||
await OnFirmaCambiata.InvokeAsync(string.Empty);
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
_dotNetRef?.Dispose();
|
||||
await ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
.signature-container {
|
||||
border: 2px dashed #d0d0e8;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
background: #fafaff;
|
||||
}
|
||||
|
||||
.signature-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
touch-action: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.signature-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0.4rem 0.6rem;
|
||||
border-top: 1px solid #ebebf5;
|
||||
}
|
||||
|
||||
.btn-clear-sig {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.75rem;
|
||||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
cursor: pointer;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
.btn-clear-sig:hover { color: #e53935; }
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
@@ -11,9 +11,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0" />
|
||||
<PackageReference Include="IntegryApiClient.Core" Version="1.0.7" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.8" />
|
||||
<PackageReference Include="IntegryApiClient.Core" Version="2.2.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="10.0.8" />
|
||||
<PackageReference Include="MudBlazor" Version="9.5.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
@@ -1,7 +1,7 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
|
||||
namespace Template.Shared;
|
||||
namespace Fixiy.Shared;
|
||||
|
||||
public static class InteractiveRenderSettings
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Template.Shared.Interfaces;
|
||||
namespace Fixiy.Shared.Interfaces;
|
||||
|
||||
public interface IFormFactor
|
||||
{
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public record Allegato(
|
||||
string Id,
|
||||
string Nome,
|
||||
string Url,
|
||||
TipoAllegato Tipo
|
||||
);
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public record AttivitaItem(
|
||||
string Id,
|
||||
PrioritaAttivita Priorita,
|
||||
string PuntoVendita,
|
||||
string Categoria,
|
||||
string Sottocategoria,
|
||||
string Reparto,
|
||||
string Luogo,
|
||||
string Descrizione,
|
||||
List<Allegato> Allegati,
|
||||
StatoAttivita Stato = StatoAttivita.Aperta,
|
||||
int Ordine = 0
|
||||
)
|
||||
{
|
||||
public bool IsLocked => Priorita == PrioritaAttivita.Emergenza;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public record OperatoreItem(
|
||||
string Id,
|
||||
string Nome
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum PrioritaAttivita
|
||||
{
|
||||
Normale,
|
||||
Alta,
|
||||
Emergenza
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum StatoAttivita
|
||||
{
|
||||
Aperta,
|
||||
Chiusa,
|
||||
Rimandata
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Fixiy.Shared.Models;
|
||||
|
||||
public enum TipoAllegato
|
||||
{
|
||||
Immagine,
|
||||
Piantina,
|
||||
Documento,
|
||||
Email
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
using Fixiy.Shared.Models;
|
||||
|
||||
namespace Fixiy.Shared.Services;
|
||||
|
||||
public class MockAttivitaService
|
||||
{
|
||||
public List<AttivitaItem> GetAttivitaOggi() =>
|
||||
[
|
||||
new(
|
||||
Id: "ATT-001",
|
||||
Priorita: PrioritaAttivita.Emergenza,
|
||||
PuntoVendita: "Carrefour Roma Prati",
|
||||
Categoria: "Impianti",
|
||||
Sottocategoria: "Elettrico",
|
||||
Reparto: "Casse",
|
||||
Luogo: "Banco casse centrali",
|
||||
Descrizione: "Guasto al quadro elettrico secondario delle casse. Interruzione totale delle attività di vendita. Intervento urgente richiesto.",
|
||||
Allegati:
|
||||
[
|
||||
new("A1", "foto_quadro_01.jpg", "https://picsum.photos/seed/quadro/400/300", TipoAllegato.Immagine),
|
||||
new("A2", "piantina_casse.png", "https://picsum.photos/seed/piantina/400/300", TipoAllegato.Piantina),
|
||||
new("A3", "segnalazione_urgente.eml", "#", TipoAllegato.Email)
|
||||
]
|
||||
),
|
||||
new(
|
||||
Id: "ATT-002",
|
||||
Priorita: PrioritaAttivita.Emergenza,
|
||||
PuntoVendita: "Esselunga Milano Viale Certosa",
|
||||
Categoria: "Refrigerazione",
|
||||
Sottocategoria: "Banco frigo",
|
||||
Reparto: "Latticini e Salumi",
|
||||
Luogo: "Corsia B — banco frigo lungo",
|
||||
Descrizione: "Perdita di gas refrigerante dal banco frigo. Temperatura in risalita, rischio deperimento merce. Evacuazione parziale del reparto in corso.",
|
||||
Allegati:
|
||||
[
|
||||
new("A4", "allarme_temp_log.eml", "#", TipoAllegato.Email),
|
||||
new("A5", "foto_perdita.jpg", "https://picsum.photos/seed/frigo/400/300", TipoAllegato.Immagine)
|
||||
]
|
||||
),
|
||||
new(
|
||||
Id: "ATT-003",
|
||||
Priorita: PrioritaAttivita.Alta,
|
||||
PuntoVendita: "Ipercoop Torino Lingotto",
|
||||
Categoria: "Edilizia",
|
||||
Sottocategoria: "Impermeabilizzazione",
|
||||
Reparto: "Magazzino",
|
||||
Luogo: "Soffitto magazzino merci — zona nord",
|
||||
Descrizione: "Infiltrazione d'acqua dal soffitto a causa delle piogge. Presenza di umidità sulle scaffalature. Necessario intervento entro 24 ore.",
|
||||
Allegati:
|
||||
[
|
||||
new("A6", "foto_infiltrazione_01.jpg", "https://picsum.photos/seed/infiltr/400/300", TipoAllegato.Immagine),
|
||||
new("A7", "foto_infiltrazione_02.jpg", "https://picsum.photos/seed/infiltr2/400/300", TipoAllegato.Immagine),
|
||||
new("A8", "piantina_magazzino.png", "https://picsum.photos/seed/mag/400/300", TipoAllegato.Piantina),
|
||||
new("A9", "relazione_tecnica.docx", "#", TipoAllegato.Documento)
|
||||
]
|
||||
),
|
||||
new(
|
||||
Id: "ATT-004",
|
||||
Priorita: PrioritaAttivita.Alta,
|
||||
PuntoVendita: "Coop Firenze Gavinana",
|
||||
Categoria: "Sicurezza",
|
||||
Sottocategoria: "Antincendio",
|
||||
Reparto: "Tutto il punto vendita",
|
||||
Luogo: "Centrale antincendio — piano interrato",
|
||||
Descrizione: "Segnalazione di falso allarme ripetuto dalla centrale antincendio. Verifica e reset del sistema richiesti prima dell'apertura.",
|
||||
Allegati:
|
||||
[
|
||||
new("A10", "storico_allarmi.eml", "#", TipoAllegato.Email)
|
||||
]
|
||||
),
|
||||
new(
|
||||
Id: "ATT-005",
|
||||
Priorita: PrioritaAttivita.Normale,
|
||||
PuntoVendita: "Pam Panorama Venezia Mestre",
|
||||
Categoria: "Manutenzione",
|
||||
Sottocategoria: "Pavimentazione",
|
||||
Reparto: "Ortofrutta",
|
||||
Luogo: "Corsie 3-4",
|
||||
Descrizione: "Sostituzione mattonelle scheggiate nella zona ortofrutta. Intervento programmato.",
|
||||
Allegati:
|
||||
[
|
||||
new("A11", "foto_pavimento.jpg", "https://picsum.photos/seed/pav/400/300", TipoAllegato.Immagine),
|
||||
new("A12", "preventivo_materiali.docx", "#", TipoAllegato.Documento)
|
||||
],
|
||||
Ordine: 0
|
||||
),
|
||||
new(
|
||||
Id: "ATT-006",
|
||||
Priorita: PrioritaAttivita.Normale,
|
||||
PuntoVendita: "Simply Market Bologna Corticella",
|
||||
Categoria: "Impianti",
|
||||
Sottocategoria: "Idraulico",
|
||||
Reparto: "Servizi igienici",
|
||||
Luogo: "Bagni personale — piano terra",
|
||||
Descrizione: "Perdita dal rubinetto del lavandino nel bagno del personale. Sostituzione guarnizioni.",
|
||||
Allegati:
|
||||
[
|
||||
new("A13", "foto_rubinetto.jpg", "https://picsum.photos/seed/rubinetto/400/300", TipoAllegato.Immagine)
|
||||
],
|
||||
Ordine: 1
|
||||
)
|
||||
];
|
||||
|
||||
public List<OperatoreItem> GetOperatoriDefault() =>
|
||||
[
|
||||
new("OP-001", "Marco Esposito"),
|
||||
new("OP-002", "Luca Ferretti")
|
||||
];
|
||||
|
||||
public List<string> GetAziendeTerme() =>
|
||||
[
|
||||
"Arneg",
|
||||
"Desich",
|
||||
"Idracol",
|
||||
"Carrier",
|
||||
"Danfoss",
|
||||
"Alfa Laval",
|
||||
"Altro"
|
||||
];
|
||||
}
|
||||
@@ -5,6 +5,9 @@
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using Microsoft.AspNetCore.Components.Web.Virtualization
|
||||
@using Microsoft.JSInterop
|
||||
@using Template.Shared.Components
|
||||
@using BlazorBootstrap;
|
||||
@using MudBlazor
|
||||
@using Fixiy.Shared.Components
|
||||
@using Fixiy.Shared.Components.SingleElements
|
||||
@using Fixiy.Shared.Models
|
||||
@using Fixiy.Shared.Services
|
||||
@using static InteractiveRenderSettings
|
||||
@@ -7,7 +7,6 @@
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
/*color: #006bb7;*/
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
@@ -23,7 +22,8 @@ a, .btn-link {
|
||||
}
|
||||
|
||||
.content {
|
||||
padding-top: 1.1rem;
|
||||
padding-top: 1.5rem;
|
||||
padding-bottom: calc(6.5rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
h1:focus {
|
||||
@@ -76,9 +76,11 @@ h1:focus {
|
||||
}
|
||||
|
||||
.page-title {
|
||||
/*text-align: center;*/
|
||||
font-size: x-large;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
color: var(--darker-color);
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@supports (-webkit-touch-callout: none) {
|
||||
@@ -92,7 +94,6 @@ h1:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
#app {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
height: 100vh;
|
||||
@@ -102,3 +103,75 @@ h1:focus {
|
||||
padding-left: env(safe-area-inset-left);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Bottom sheet — custom MudDialog (ChiusuraModal, AllegatiModal) ─── */
|
||||
|
||||
.mud-dialog.bottom-sheet {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
max-height: 92vh;
|
||||
border-radius: 24px 24px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||
box-shadow: 0 -8px 40px rgba(31, 30, 90, 0.22);
|
||||
animation: bottom-sheet-up 0.28s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
}
|
||||
|
||||
.mud-dialog.bottom-sheet .mud-dialog-title {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mud-dialog.bottom-sheet .mud-dialog-content {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@keyframes bottom-sheet-up {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.sheet-grabber {
|
||||
width: 40px;
|
||||
height: 4px;
|
||||
border-radius: 100px;
|
||||
background: #d8d8e4;
|
||||
margin: 0.6rem auto 0.1rem;
|
||||
}
|
||||
|
||||
.sheet-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.6rem 1.2rem 0.7rem;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.sheet-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 700;
|
||||
color: var(--darker-color);
|
||||
}
|
||||
|
||||
.sheet-close {
|
||||
background: #f0f0f5 !important;
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
|
||||