12 Commits
Author SHA1 Message Date
MarcoE f59c8d3aaf Finish v1.2.3(9) 2026-10-21 08:51:50 +02:00
MarcoE 42e2403362 Finish v1.2.3(9) 2026-10-21 08:51:50 +02:00
MarcoE 249b301920 -> 1.2.3 (9) 2026-10-21 08:51:43 +02:00
MarcoEandClaude Opus 5.5 885b3cc6b6 Avvisa quando manca il permesso della fotocamera
Con il permesso negato "Aggiungi una foto" non faceva nulla: il servizio
restituiva null come per un annullamento. Dopo un rifiuto definitivo
Android non ripropone la richiesta, quindi il rilevatore restava bloccato
senza capire perche'.

- AttachedService: la richiesta dei permessi diventa RequestCameraPermissions,
  usata anche da SelectImageFromCamera; OpenAppSettings apre le impostazioni
  dell'app.
- ModalFormScheda e ModalAddAttached: chiedono i permessi prima di scattare
  e, se negati, mostrano un avviso con l'azione "Impostazioni".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 08:49:34 +02:00
MarcoEandClaude Opus 5.5 7e1492d118 Avvisa quando la fotocamera non si apre e passa alla galleria
Se lo scatto falliva, AttachedService restituiva null come per un
annullamento: il rilevatore toccava "Aggiungi una foto" e non succedeva
nulla.

- AttachedService: espone IsCameraAvailable (IsCaptureSupported) e, dopo
  aver registrato l'errore e averlo inviato a Sentry, lo rilancia invece di
  restituire null.
- ModalFormScheda: dalla scheda il pulsante apre solo la fotocamera, quindi
  senza fotocamera non c'era alternativa. Ora avvisa e apre la galleria;
  se lo scatto fallisce mostra un errore.
- ModalAddAttached: stessa gestione dell'errore di scatto.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 08:45:57 +02:00
MarcoEandClaude Opus 5.5 fd0c218ab5 Dichiara la fotocamera di sistema nelle queries del manifest
Da Android 11 un'app vede le altre app installate solo se le dichiara in
<queries>. Senza l'intent IMAGE_CAPTURE, MediaPicker.CapturePhotoAsync non
trovava la fotocamera di sistema e falliva con "Either there was no camera
on the device or 'android.media.action.IMAGE_CAPTURE' was not added to the
<queries> element" (segnalato su Sentry).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 08:45:57 +02:00
MarcoE deda374b5b Finish v1.2.2 2026-09-29 10:34:45 +02:00
MarcoE e1ca03f646 Finish v1.2.2 2026-09-29 10:34:45 +02:00
MarcoE 92271e20fb -> v1.2.2 (8) 2026-09-29 10:34:40 +02:00
MarcoEandClaude Opus 5.5 e524c12a96 Riporta a bozza le schede non inviate senza reparto o motivo
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:33:01 +02:00
MarcoEandClaude Opus 5.5 de889ef8c7 Mostra nell'ispezione le bozze senza reparto
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-29 10:27:22 +02:00
MarcoE 0e00fb273a Finish v1.2.1 2026-09-28 12:11:37 +02:00
9 changed files with 153 additions and 26 deletions

No files matched your search

@@ -155,14 +155,29 @@ public class IspezioniService(AppDbContext db) : IIspezioniService
await db.SaveChangesAsync(); await db.SaveChangesAsync();
} }
public Task<List<Scheda>> GetAllSchedeOfIspezioneAsync(string codMdep, DateTime data, string rilevatore) => public async Task<List<Scheda>> GetAllSchedeOfIspezioneAsync(string codMdep, DateTime data, string rilevatore)
db.Schede {
// Le versioni precedenti dell'app hanno lasciato schede non bozza senza
// reparto o motivo: risultano inviabili ma il server le rifiuta, e
// bloccano la conclusione dell'ispezione. Tornano bozze, cosi' il
// rilevatore le completa. Quelle gia' inviate non si toccano.
await db.Schede
.Where(x => x.CodMdep == codMdep &&
x.Data == data &&
x.Rilevatore == rilevatore &&
!x.Bozza &&
x.ActivityId == null &&
(string.IsNullOrEmpty(x.CodJfas) || string.IsNullOrEmpty(x.ActivityTypeId)))
.ExecuteUpdateAsync(s => s.SetProperty(x => x.Bozza, true));
return await db.Schede
.AsNoTracking() .AsNoTracking()
.Include(s => s.Articoli) .Include(s => s.Articoli)
.Where(x => x.CodMdep == codMdep && .Where(x => x.CodMdep == codMdep &&
x.Data == data && x.Data == data &&
x.Rilevatore == rilevatore) x.Rilevatore == rilevatore)
.ToListAsync(); .ToListAsync();
}
public Task<Scheda?> GetSchedaAsync(int schedaId) => public Task<Scheda?> GetSchedaAsync(int schedaId) =>
db.Schede db.Schede
+15 -3
View File
@@ -8,12 +8,21 @@ namespace SteUp.Maui.Core.Services;
public class AttachedService(ILogger<FileManager> logger) : IAttachedService public class AttachedService(ILogger<FileManager> logger) : IAttachedService
{ {
public async Task<AttachedDto?> SelectImageFromCamera() public bool IsCameraAvailable => MediaPicker.Default.IsCaptureSupported;
public async Task<bool> RequestCameraPermissions()
{ {
var cameraPerm = await Permissions.RequestAsync<Permissions.Camera>(); var cameraPerm = await Permissions.RequestAsync<Permissions.Camera>();
var storagePerm = await Permissions.RequestAsync<Permissions.StorageWrite>(); var storagePerm = await Permissions.RequestAsync<Permissions.StorageWrite>();
if (cameraPerm != PermissionStatus.Granted || storagePerm != PermissionStatus.Granted) return cameraPerm == PermissionStatus.Granted && storagePerm == PermissionStatus.Granted;
}
public void OpenAppSettings() => AppInfo.Current.ShowSettingsUI();
public async Task<AttachedDto?> SelectImageFromCamera()
{
if (!await RequestCameraPermissions())
return null; return null;
FileResult? result; FileResult? result;
@@ -28,7 +37,10 @@ public class AttachedService(ILogger<FileManager> logger) : IAttachedService
logger.LogError(ex, ex.Message); logger.LogError(ex, ex.Message);
Console.WriteLine($"Errore cattura foto: {ex.Message}"); Console.WriteLine($"Errore cattura foto: {ex.Message}");
SentrySdk.CaptureException(ex); SentrySdk.CaptureException(ex);
return null;
// Rilancia: restituire null qui era indistinguibile da un annullamento,
// e il rilevatore toccava "Aggiungi una foto" senza vedere nulla.
throw;
} }
return result is null ? null : await UtilityFile.ConvertToDto(result, AttachedDto.TypeAttached.Image); return result is null ? null : await UtilityFile.ConvertToDto(result, AttachedDto.TypeAttached.Image);
@@ -25,4 +25,11 @@
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<!-- Android 11+: rende visibile l'app fotocamera di sistema a MediaPicker -->
<queries>
<intent>
<action android:name="android.media.action.IMAGE_CAPTURE" />
</intent>
</queries>
</manifest> </manifest>
+2 -2
View File
@@ -30,8 +30,8 @@
<ApplicationId>it.integry.SteUp</ApplicationId> <ApplicationId>it.integry.SteUp</ApplicationId>
<!-- Versions --> <!-- Versions -->
<ApplicationDisplayVersion>1.2.1</ApplicationDisplayVersion> <ApplicationDisplayVersion>1.2.3</ApplicationDisplayVersion>
<ApplicationVersion>7</ApplicationVersion> <ApplicationVersion>9</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion> <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<!-- <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">--> <!-- <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">-->
@@ -157,6 +157,7 @@
@foreach (var group in SchedeGrouped) @foreach (var group in SchedeGrouped)
{ {
var reparto = group.Key; var reparto = group.Key;
var senzaReparto = ReferenceEquals(reparto, SenzaReparto);
var chiuso = IsCollapsed(reparto); var chiuso = IsCollapsed(reparto);
var daInviare = group.Value.CountDaInviare(); var daInviare = group.Value.CountDaInviare();
@@ -172,7 +173,9 @@
</button> </button>
<div class="reparto__actions"> <div class="reparto__actions">
@if (!IsCompleted) @* Il gruppo "Senza reparto" raccoglie solo bozze da
sistemare: non ha un "+" ne' un invio suo. *@
@if (!IsCompleted && !senzaReparto)
{ {
@* Da qui la scheda nasce gia' con il suo reparto: e' @* Da qui la scheda nasce gia' con il suo reparto: e'
l'unica differenza con il "+" della barra. *@ l'unica differenza con il "+" della barra. *@
@@ -326,13 +329,15 @@
// schermata, non del documento. // schermata, non del documento.
private readonly HashSet<string> _collapsed = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _collapsed = new(StringComparer.OrdinalIgnoreCase);
private bool IsCollapsed(JtbFasiDto reparto) => // Il gruppo "Senza reparto" non ha codice: la chiave vuota e' la sua.
reparto.CodJfas is not null && _collapsed.Contains(reparto.CodJfas); private static string CollapseKey(JtbFasiDto reparto) => reparto.CodJfas ?? string.Empty;
private bool IsCollapsed(JtbFasiDto reparto) => _collapsed.Contains(CollapseKey(reparto));
private void Toggle(JtbFasiDto reparto) private void Toggle(JtbFasiDto reparto)
{ {
if (reparto.CodJfas is null) return; var key = CollapseKey(reparto);
if (!_collapsed.Remove(reparto.CodJfas)) _collapsed.Add(reparto.CodJfas); if (!_collapsed.Remove(key)) _collapsed.Add(key);
} }
private static string Conteggi(List<Scheda> schede) private static string Conteggi(List<Scheda> schede)
@@ -542,15 +547,22 @@
StateHasChanged(); StateHasChanged();
}); });
// Una bozza si puo' salvare anche senza reparto: senza questo gruppo
// sparirebbe dall'elenco pur restando sul dispositivo e contata fra le bozze.
private static readonly JtbFasiDto SenzaReparto = new() { Descrizione = "Senza reparto" };
private void GroupSchede() private void GroupSchede()
{ {
SchedeGrouped = SchedeList var grouped = new Dictionary<JtbFasiDto, List<Scheda>>();
.Where(s => s.Reparto != null)
.GroupBy(s => s.CodJfas) // In testa: sono schede a meta' che chiedono di essere completate.
.ToDictionary( var senzaReparto = SchedeList.FindAll(s => s.Reparto == null);
g => g.First().Reparto!, if (senzaReparto.Count > 0) grouped[SenzaReparto] = senzaReparto;
g => g.ToList()
); foreach (var g in SchedeList.Where(s => s.Reparto != null).GroupBy(s => s.CodJfas))
grouped[g.First().Reparto!] = g.ToList();
SchedeGrouped = grouped;
} }
private void OnSchedaModified(Scheda obj) private void OnSchedaModified(Scheda obj)
@@ -16,7 +16,7 @@
<div class="list-card" aria-label="@SemanticLabel"> <div class="list-card" aria-label="@SemanticLabel">
<div class="list-card__head"> <div class="list-card__head">
<div class="list-card__ident"> <div class="list-card__ident">
<div class="t-ident">@Scheda.DescrizioneReparto</div> <div class="t-ident">@(Scheda.DescrizioneReparto ?? "Senza reparto")</div>
<h3 class="list-card__title">@Titolo</h3> <h3 class="list-card__title">@Titolo</h3>
</div> </div>
</div> </div>
@@ -125,7 +125,7 @@
}; };
private string SemanticLabel => private string SemanticLabel =>
$"Scheda {Titolo}, reparto {Scheda.DescrizioneReparto}, {SyncState.ToBadge().Label}, {ArticoliLabel}, {FotoLabel}"; $"Scheda {Titolo}, reparto {Scheda.DescrizioneReparto ?? "non indicato"},{SyncState.ToBadge().Label}, {ArticoliLabel}, {FotoLabel}";
private async Task SetBusy(bool value) private async Task SetBusy(bool value)
{ {
@@ -40,7 +40,32 @@
private async Task OnCamera() private async Task OnCamera()
{ {
var selectImageFromCamera = await AttachedService.SelectImageFromCamera(); if (!await AttachedService.RequestCameraPermissions())
{
Snackbar.Clear();
Snackbar.Add("Per scattare le foto serve il permesso della fotocamera", Severity.Warning, config =>
{
config.Action = "Impostazioni";
config.OnClick = _ =>
{
AttachedService.OpenAppSettings();
return Task.CompletedTask;
};
});
return;
}
AttachedDto? selectImageFromCamera;
try
{
selectImageFromCamera = await AttachedService.SelectImageFromCamera();
}
catch (Exception)
{
Snackbar.Clear();
Snackbar.Add("Non è stato possibile scattare la foto, riprova", Severity.Error);
return;
}
if (selectImageFromCamera != null) if (selectImageFromCamera != null)
{ {
@@ -754,12 +754,60 @@
#region File #region File
// Dopo un rifiuto definitivo Android non ripropone la richiesta: il
// permesso si riattiva solo dalle impostazioni, quindi le apriamo noi.
private void ShowCameraPermissionDenied()
{
Snackbar.Clear();
Snackbar.Add("Per scattare le foto serve il permesso della fotocamera", Severity.Warning, config =>
{
config.Action = "Impostazioni";
config.OnClick = _ =>
{
AttachedService.OpenAppSettings();
return Task.CompletedTask;
};
});
}
private async Task OpenAddAttached() private async Task OpenAddAttached()
{ {
var selectImageFromCamera = await AttachedService.SelectImageFromCamera(); List<AttachedDto>? attachedList;
if (selectImageFromCamera == null) return;
if (AttachedService.IsCameraAvailable)
var attachedList = new List<AttachedDto> { selectImageFromCamera }; {
if (!await AttachedService.RequestCameraPermissions())
{
ShowCameraPermissionDenied();
return;
}
AttachedDto? selectImageFromCamera;
try
{
selectImageFromCamera = await AttachedService.SelectImageFromCamera();
}
catch (Exception)
{
// Gia' registrato e inviato a Sentry dal servizio
Snackbar.Clear();
Snackbar.Add("Non è stato possibile scattare la foto, riprova", Severity.Error);
return;
}
if (selectImageFromCamera == null) return;
attachedList = [selectImageFromCamera];
}
else
{
// Senza fotocamera "Aggiungi una foto" non avrebbe altra strada:
// si passa alla galleria invece di lasciare il rilevatore fermo.
Snackbar.Clear();
Snackbar.Add("Fotocamera non disponibile: scegli la foto dalla galleria", Severity.Warning);
attachedList = await AttachedService.SelectImageFromGallery();
if (attachedList is null || attachedList.Count == 0) return;
}
OnLoading = true; OnLoading = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
@@ -4,6 +4,14 @@ namespace SteUp.Shared.Core.Interface.System;
public interface IAttachedService public interface IAttachedService
{ {
bool IsCameraAvailable { get; }
/// <summary>Chiede i permessi per scattare; false se negati.</summary>
Task<bool> RequestCameraPermissions();
void OpenAppSettings();
/// <summary>Null se l'utente annulla; lancia se lo scatto fallisce.</summary>
Task<AttachedDto?> SelectImageFromCamera(); Task<AttachedDto?> SelectImageFromCamera();
Task<List<AttachedDto>?> SelectImageFromGallery(); Task<List<AttachedDto>?> SelectImageFromGallery();
} }