glanzlicht / MACHWEIS

Mach’s. Beleg’s. Rechne es ab. MACHWEIS ist eine Local-First-Flutter-App für schnelle, kontrollierbare Leistungsnachweise im Handwerk. Die Oberfläche läuft auf iOS, Android und im Web und ist vollständig deutschsprachig.

Verified secure showcase
180 Files 605 KB Public size 26 Redacted Dart · PHP · SQL · Markdown · XML · PLIST

Project overview

MACHWEIS is a verified code showcase with 180 visible project entries across 8 top-level modules. The primary detected language is Dart; protected files remain visible in the structure while their contents stay private.

README

MACHWEIS

Mach’s. Beleg’s. Rechne es ab.

Produktansicht

MACHWEIS Dashboard auf Laptop und Smartphone

MACHWEIS ist eine Local-First-Flutter-App für schnelle, kontrollierbare Leistungsnachweise im Handwerk. Die Oberfläche läuft auf iOS, Android und im Web und ist vollständig deutschsprachig.

Aktueller Funktionsumfang

  • lokale Firmen-, Kunden-, Auftrags- und Mitarbeiterdaten (SQLite auf Android/iOS, Browser-Speicher im Web)
  • mobile Stammdaten-Synchronisation für Kunden, Baustellen, Aufträge, Mitarbeiterprofile und Auftragszuweisungen über die serverseitigen Push-/Pull-Endpunkte
  • atomare lokale Änderungen mit Outbox, Cursor, Retry, sichtbaren Konflikten und einer ausdrücklichen Importentscheidung bei der ersten Verbindung
  • unveränderliche Berichtsversionen mit sichtbarem Prüfverlauf
  • persistente Offline-Synchronisationswarteschlange mit Retry-Strategie
  • lokale Fotodokumentation per Kamera, Galerie oder Web-Dateiauswahl
  • gebrandete PDF-Leistungsnachweise mit Unterschrift, Fotodokumentation und vorbereitetem Teilen per E-Mail
  • prüfpflichtiger KI-Assistent für Baustellennotizen mit OpenAI, Google Gemini oder eigenem Proxy
  • Rollen: Inhaber, Büro, Bauleiter und Monteur
  • strukturierte Arbeitszeit mit Leistungsdatum, Pause und Nettozeit
  • mehrere Leistungs- und Materialpositionen
  • Zusatzaufträge mit Kundenbestätigung
  • Vollständigkeitsprüfung vor Unterschrift
  • digitale Kundenunterschrift und unveränderlicher Abschluss
  • CSV-Export und vorbereitete PDF-Renderer-Schnittstelle
  • responsive Smartphone- und Webnavigation
  • aktuelle App-Version und Build-Nummer dauerhaft am unteren Rand der Navigation

Lokale Entwicklung

flutter pub get
flutter analyze
flutter test
flutter run -d chrome

Produktkontext und technische Leitplanken stehen in [docs/PRODUCT_BRIEF.md](docs/PRODUCT_BRIEF.md). Architektur und Serververtrag der KI-Unterstützung beschreibt [docs/AI_INTEGRATION.md](docs/AI_INTEGRATION.md).

Die Stammdaten-Synchronisation ist ausschließlich für iOS und Android aktiviert. Sie verwendet das Access-Token der mobilen Anmeldung und erneuert es bei Bedarf über die bestehende Authentifizierung. Im Web bleibt die Synchronisation bewusst deaktiviert.

Backend-Grundgerüst

Das separate PHP-8.4-REST-Grundgerüst liegt in [backend/](backend/README.md). Es enthält in diesem Stand ausschließlich Infrastruktur und den Health-Endpunkt; Login, Fachlogik und KI sind noch nicht enthalten.

lib/features/reports/data/demo_report_store.dartDart
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../../../core/auth/auth_store.dart';
import '../../../core/storage/local_storage.dart';
import '../../../core/identifiers/uuid.dart';
import '../../../core/sync/sync_queue_store.dart';
import '../../../core/sync/report_sync.dart';
import '../../../core/sync/report_asset_sync.dart';
import '../../jobs/data/job_store.dart';
import 'report_photo_store.dart';
import '../../../core/web/web_office_api.dart';
class MaterialEntry {
const MaterialEntry({
required this.name,
required this.quantity,
required this.unit,
});
final String name;
final String quantity;
final String unit;
Map<String, String> toJson() => {
'name': name,
'quantity': quantity,
'unit': unit,
};
factory MaterialEntry.fromJson(Map<String, dynamic> json) => MaterialEntry(
name: json['name'] as String? ?? '',
quantity: json['quantity'] as String? ?? '',
unit: json['unit'] as String? ?? 'Stk.',
);
}
class DemoReport {
const DemoReport({
required this.id,
required this.customer,
required this.site,
required this.activity,
required this.material,
required this.quantity,
required this.date,
required this.start,
required this.end,
this.isFinalized = false,
this.finalizedAt,
this.signature = const [],
this.participantIds = const [],
this.activityItems = const [],
this.materialItems = const [],
this.additionalOrder = '',
this.additionalOrderApproved = false,
this.breakMinutes = 0,
this.version = 1,
this.photoIds = const [],
this.fileIds = const [],
this.fileNames = const [],
this.customerId,
this.siteId,
this.jobId,
this.signatureAssetId,
});
final String id;
final String customer;
final String site;
final String activity;
final String material;
final String quantity;
final DateTime date;
final TimeOfDay start;
final TimeOfDay end;
final bool isFinalized;
final DateTime? finalizedAt;
final List<List<double>> signature;
final List<String> participantIds;
final List<String> activityItems;
final List<MaterialEntry> materialItems;
final String additionalOrder;
final bool additionalOrderApproved;
final int breakMinutes;
final int version;
final List<String> photoIds;
final List<String> fileIds;
final List<String> fileNames;
final String? customerId;
final String? siteId;
final String? jobId;
final String? signatureAssetId;
int get grossMinutes {
final startValue = start.hour * 60 + start.minute;
final endValue = end.hour * 60 + end.minute;
return endValue - startValue;
}
int get netMinutes => grossMinutes - breakMinutes;
Map<String, Object?> toJson() => {
'id': id,
'customer': customer,
'site': site,
'activity': activity,
'material': material,
'quantity': quantity,
'date': date.toIso8601String(),
'startHour': start.hour,
'startMinute': start.minute,
'endHour': end.hour,
'endMinute': end.minute,
'isFinalized': isFinalized,
'finalizedAt': finalizedAt?.toIso8601String(),
'signature': signature,
'participantIds': participantIds,
'activityItems': activityItems,
'materialItems': materialItems.map((item) => item.toJson()).toList(),
'additionalOrder': additionalOrder,
'additionalOrderApproved': additionalOrderApproved,
'breakMinutes': breakMinutes,
'version': version,
'photoIds': photoIds,
'fileIds': fileIds,
'fileNames': fileNames,
'customerId': customerId,
'siteId': siteId,
'jobId': jobId,
'signatureAssetId': signatureAssetId,
};
factory DemoReport.fromJson(Map<String, dynamic> json) => DemoReport(
id: json['id'] as String,
customer: json['customer'] as String,
site: json['site'] as String,
activity: json['activity'] as String,
material: json['material'] as String? ?? '',
quantity: json['quantity'] as String? ?? '',
date: DateTime.parse(json['date'] as String),
start: TimeOfDay(
hour: json['startHour'] as int,
minute: json['startMinute'] as int,
),
end: TimeOfDay(
hour: json['endHour'] as int,
minute: json['endMinute'] as int,
),
isFinalized: json['isFinalized'] as bool? ?? false,
finalizedAt: json['finalizedAt'] == null
? null
: DateTime.parse(json['finalizedAt'] as String),
signature: (json['signature'] as List<dynamic>? ?? [])
.map(
(point) => (point as List<dynamic>)
.map((value) => (value as num).toDouble())
.toList(),
)
.toList(),
participantIds:
(json['participantIds'] as List<dynamic>? ?? const ['demo-max'])
.cast<String>(),
activityItems:
(json['activityItems'] as List<dynamic>? ?? [json['activity']])
.whereType<String>()
.where((value) => value.isNotEmpty)
.toList(),
materialItems: json['materialItems'] is List<dynamic>
? (json['materialItems'] as List<dynamic>)
.map(
(value) =>
MaterialEntry.fromJson(value as Map<String, dynamic>),
)
.toList()
: (json['material'] as String? ?? '').isEmpty
? const []
: [
MaterialEntry(
name: json['material'] as String,
quantity: json['quantity'] as String? ?? '',
unit: 'Stk.',
),
],
additionalOrder: json['additionalOrder'] as String? ?? '',
additionalOrderApproved: json['additionalOrderApproved'] as bool? ?? false,
breakMinutes: json['breakMinutes'] as int? ?? 0,
version: json['version'] as int? ?? 1,
photoIds: (json['photoIds'] as List<dynamic>? ?? const []).cast<String>(),
fileIds: (json['fileIds'] as List<dynamic>? ?? const []).cast<String>(),
fileNames: (json['fileNames'] as List<dynamic>? ?? const []).cast<String>(),
customerId: json['customerId'] as String?,
siteId: json['siteId'] as String?,
jobId: json['jobId'] as String?,
signatureAssetId: json['signatureAssetId'] as String?,
);
DemoReport withVersion(int nextVersion) => DemoReport(
id: id,
customer: customer,
site: site,
activity: activity,
material: material,
quantity: quantity,
date: date,
start: start,
end: end,
isFinalized: isFinalized,
finalizedAt: finalizedAt,
signature: signature,
participantIds: participantIds,
activityItems: activityItems,
materialItems: materialItems,
additionalOrder: additionalOrder,
additionalOrderApproved: additionalOrderApproved,
breakMinutes: breakMinutes,
version: nextVersion,
photoIds: photoIds,
fileIds: fileIds,
fileNames: fileNames,
customerId: customerId,
siteId: siteId,
jobId: jobId,
signatureAssetId: signatureAssetId,
);
DemoReport finalized(List<List<double>> signatureData) => DemoReport(
id: id,
customer: customer,
site: site,
activity: activity,
material: material,
quantity: quantity,
date: date,
start: start,
end: end,
isFinalized: true,
finalizedAt: DateTime.now(),
signature: signatureData,
participantIds: participantIds,
activityItems: activityItems,
materialItems: materialItems,
additionalOrder: additionalOrder,
additionalOrderApproved: additionalOrderApproved,
breakMinutes: breakMinutes,
version: version + 1,
photoIds: photoIds,
fileIds: fileIds,
fileNames: fileNames,
customerId: customerId,
siteId: siteId,
jobId: jobId,
signatureAssetId: newUuid(),
);
DemoReport withReferences({
String? customerId,
String? siteId,
String? jobId,
}) => DemoReport(
id: id,
customer: customer,
site: site,
activity: activity,
material: material,
quantity: quantity,
date: date,
start: start,
end: end,
isFinalized: isFinalized,
finalizedAt: finalizedAt,
signature: signature,
participantIds: participantIds,
activityItems: activityItems,
materialItems: materialItems,
additionalOrder: additionalOrder,
additionalOrderApproved: additionalOrderApproved,
breakMinutes: breakMinutes,
version: version,
photoIds: photoIds,
fileIds: fileIds,
fileNames: fileNames,
customerId: customerId ?? this.customerId,
siteId: siteId ?? this.siteId,
jobId: jobId ?? this.jobId,
signatureAssetId: signatureAssetId,
);
}
enum ReportAuditAction { created, updated, finalized, deleted }
class ReportAuditEntry {
const ReportAuditEntry({
required this.id,
required this.reportId,
required this.action,
required this.timestamp,
required this.version,
required this.snapshot,
});
final String id;
final String reportId;
final ReportAuditAction action;
final DateTime timestamp;
final int version;
final DemoReport snapshot;
String get label => switch (action) {
ReportAuditAction.created => 'Entwurf erstellt',
ReportAuditAction.updated => 'Entwurf bearbeitet',
ReportAuditAction.finalized => 'Vom Kunden unterschrieben',
ReportAuditAction.deleted => 'Entwurf gelöscht',
};
Map<String, Object?> toJson() => {
'id': id,
'reportId': reportId,
'action': action.name,
'timestamp': timestamp.toIso8601String(),
'version': version,
'snapshot': snapshot.toJson(),
};
factory ReportAuditEntry.fromJson(Map<String, dynamic> json) {
final actionName = json['action'] as String? ?? 'created';
return ReportAuditEntry(
id: json['id'] as String,
reportId: json['reportId'] as String,
action: ReportAuditAction.values.firstWhere(
(value) => value.name == actionName,
orElse: () => ReportAuditAction.created,
),
timestamp: DateTime.parse(json['timestamp'] as String),
version: json['version'] as int? ?? 1,
snapshot: DemoReport.fromJson(json['snapshot'] as Map<String, dynamic>),
);
}
}
class DemoReportStore extends ChangeNotifier {
DemoReportStore._();
static final instance = DemoReportStore._();
static const _storageKey = 'machweis_reports_repository_v2';
static const _legacyStorageKey = 'machweis_demo_reports_v1';
final List<DemoReport> _reports = [];
final List<ReportAuditEntry> _history = [];
int webPage = 1;
int webLimit = 10;
int webTotal = 0;
String webSearch = '';
String? webStatus;
List<DemoReport> get reports => List.unmodifiable(_reports);
List<ReportAuditEntry> historyFor(String reportId) => List.unmodifiable(
_history.where((entry) => entry.reportId == reportId).toList()
..sort((a, b) => b.timestamp.compareTo(a.timestamp)),
);
Future<void> initialize() async {
if (kIsWeb && AuthStore.instance.session != null) {
try {
final result = await WebOfficeApi.instance.listReports();
_reports
..clear()
..addAll(result.items.map(_fromServerRow));
webPage = result.page;
webLimit = result.limit;
webTotal = result.total;
_history.clear();
} on Object {
// Keep the last loaded web snapshot visible on transient errors.
}
notifyListeners();
return;
}
if (LocalStorage.instance.usesRelationalStore) {
final repository = LocalReportRepository();
final loaded = await repository.readAll();
if (loaded.reports.isNotEmpty || loaded.history.isNotEmpty) {
_reports
..clear()
..addAll(loaded.reports);
_history
..clear()
..addAll(loaded.history);
return;
}
if (AuthStore.instance.session != null) {
_reports.clear();
_history.clear();
notifyListeners();
return;
}
await _migrateJsonRepository(repository);
return;
}
final stored = await LocalStorage.instance.getString(_storageKey);
if (stored == null) {
await _migrateLegacyReports();
return;
}
try {
final repository = jsonDecode(stored) as Map<String, dynamic>;
final values = repository['reports'] as List<dynamic>? ?? const [];
final history = repository['history'] as List<dynamic>? ?? const [];
_reports
..clear()
..addAll(
values.map(
(value) => DemoReport.fromJson(value as Map<String, dynamic>),
),
);
_history
..clear()
..addAll(
history.map(
(value) => ReportAuditEntry.fromJson(value as Map<String, dynamic>),
),
);
_linkLegacyReports();
} on Object {
await LocalStorage.instance.remove(_storageKey);
}
}
Future<void> loadWebReports({
String search = '',
String? status,
int page = 1,
}) async {
if (!kIsWeb || AuthStore.instance.session == null) return;
final result = await WebOfficeApi.instance.listReports(
search: search,
status: status,
page: page,
limit: webLimit,
);
_reports
..clear()
..addAll(result.items.map(_fromServerRow));
webSearch = search;
webStatus = status;
webPage = result.page;
webTotal = result.total;
_history.clear();
notifyListeners();
}
DemoReport _fromServerRow(Map<String, dynamic> row) {
final payload = Map<String, dynamic>.from(
row['payload'] as Map? ?? const {},
);
final merged = <String, dynamic>{
...payload,
'id': row['id'] ?? payload['id'],
'version': row['current_version'] ?? payload['version'] ?? 1,
'customer': payload['customer'] ?? '',
'site': payload['site'] ?? 'Nachweis',
'activity': payload['activity'] ?? '',
'date':
payload['date'] ??
row['created_at'] ??
DateTime.now().toIso8601String(),
'startHour': payload['startHour'] ?? 0,
'startMinute': payload['startMinute'] ?? 0,
'endHour': payload['endHour'] ?? 0,
'endMinute': payload['endMinute'] ?? 0,
'isFinalized':
row['status'] == 'finalized' || payload['isFinalized'] == true,
};
return DemoReport.fromJson(merged);
}
Future<void> _migrateLegacyReports() async {
final stored = await LocalStorage.instance.getString(_legacyStorageKey);
if (stored == null) return;
try {
final values = jsonDecode(stored) as List<dynamic>;
_reports
..clear()
..addAll(
values.map(
(value) => DemoReport.fromJson(value as Map<String, dynamic>),
),
);
_linkLegacyReports();
final migratedAt = DateTime.now();
_history
..clear()
..addAll(
_reports.map(
(report) => _auditEntry(
report,
report.isFinalized
? ReportAuditAction.finalized
: ReportAuditAction.created,
timestamp: migratedAt,
),
),
);
await _persist();
await LocalStorage.instance.remove(_legacyStorageKey);
} on Object {
await LocalStorage.instance.remove(_legacyStorageKey);
}
}
Future<void> add(DemoReport report) async {
final initial = report.withVersion(1);
if (kIsWeb && AuthStore.instance.session != null) {
await WebOfficeApi.instance.createReport({
'jobId': initial.jobId,
'status': initial.isFinalized ? 'finalized' : 'draft',
'payload': Map<String, dynamic>.from(initial.toJson()),
});
await initialize();
return;
}
_reports.insert(0, initial);
_history.add(_auditEntry(initial, ReportAuditAction.created));
notifyListeners();
await _persist();
await _enqueue(initial, SyncAction.create);
}
Future<void> remove(String id) async {
final index = _reports.indexWhere((item) => item.id == id);
if (index == -1 || _reports[index].isFinalized) return;
final report = _reports[index];
if (kIsWeb && AuthStore.instance.session != null) {
await WebOfficeApi.instance.deleteReport(report.id);
await initialize();
return;
}
_reports.removeWhere((report) => report.id == id);
_history.add(_auditEntry(report, ReportAuditAction.deleted));
notifyListeners();
await _persist();
final tenantId = AuthStore.instance.session?.tenantId;
await SyncQueueStore.instance.enqueue(
entityType: 'report',
entityId: report.id,
action: SyncAction.delete,
payload: {'id': report.id, 'version': report.version},
tenantId: tenantId,
baseRevision: tenantId == null
? 0
: SyncQueueStore.instance.reportRevision(tenantId, report.id),
);
await ReportSyncController.instance.synchronize();
for (final photoId in report.photoIds) {
await ReportPhotoStore.instance.remove(photoId);
}
for (final fileId in report.fileIds) {
await ReportAssetSyncController.instance.enqueueDelete(
assetId: fileId,
reportId: report.id,
);
}
if (report.fileIds.isNotEmpty) {
await ReportAssetSyncController.instance.synchronize();
}
}
Future<void> update(DemoReport report) async {
final index = _reports.indexWhere((item) => item.id == report.id);
if (index == -1) return;
if (_reports[index].isFinalized) return;
final updated = report.withVersion(_reports[index].version + 1);
if (kIsWeb && AuthStore.instance.session != null) {
await WebOfficeApi.instance.addReportVersion(
updated.id,
Map<String, dynamic>.from(updated.toJson()),
);
await initialize();
return;
}
_reports[index] = updated;
_history.add(_auditEntry(updated, ReportAuditAction.updated));
notifyListeners();
await _persist();
await _enqueue(updated, SyncAction.update);
}
Future<void> finalize(String id, List<List<double>> signature) async {
final index = _reports.indexWhere((item) => item.id == id);
if (index == -1 || _reports[index].isFinalized) return;
final finalized = _reports[index].finalized(signature);
if (kIsWeb && AuthStore.instance.session != null) {
await WebOfficeApi.instance.addReportVersion(
finalized.id,
Map<String, dynamic>.from(finalized.toJson()),
status: 'finalized',
);
await initialize();
return;
}
_reports[index] = finalized;
_history.add(_auditEntry(_reports[index], ReportAuditAction.finalized));
notifyListeners();
await _persist();
await _enqueue(_reports[index], SyncAction.finalize);
final persisted = _reports[index];
if (persisted.signatureAssetId != null) {
await ReportAssetSyncController.instance.enqueueSignature(
assetId: persisted.signatureAssetId!,
reportId: persisted.id,
points: persisted.signature,
);
await ReportAssetSyncController.instance.synchronize();
}
}
Future<void> _persist() async {
if (LocalStorage.instance.usesRelationalStore) {
await LocalReportRepository().replaceAll(_reports, _history);
return;
}
await LocalStorage.instance.setString(
_storageKey,
jsonEncode({
'reports': _reports.map((report) => report.toJson()).toList(),
'history': _history.map((entry) => entry.toJson()).toList(),
}),
);
}
Future<void> _migrateJsonRepository(LocalReportRepository repository) async {
final stored = await LocalStorage.instance.getString(_storageKey);
if (stored != null) {
try {
final value = jsonDecode(stored) as Map<String, dynamic>;
_reports
..clear()
..addAll(
(value['reports'] as List<dynamic>? ?? const []).map(
(item) => DemoReport.fromJson(item as Map<String, dynamic>),
),
);
_history
..clear()
..addAll(
(value['history'] as List<dynamic>? ?? const []).map(
(item) => ReportAuditEntry.fromJson(item as Map<String, dynamic>),
),
);
_linkLegacyReports();
await repository.replaceAll(_reports, _history);
return;
} on Object {
// Fall through to the older report format without deleting either.
}
}
await _migrateLegacyReports();
if (_reports.isNotEmpty || _history.isNotEmpty) {
await repository.replaceAll(_reports, _history);
}
}
void _linkLegacyReports() {
for (var index = 0; index < _reports.length; index++) {
final report = _reports[index];
if (report.customerId != null &&
report.siteId != null &&
report.jobId != null) {
continue;
}
final matches = JobStore.instance.jobs
.where(
(job) =>
job.customer.trim() == report.customer.trim() &&
job.site.trim() == report.site.trim(),
)
.toList();
if (matches.length == 1) {
final job = matches.single;
_reports[index] = report.withReferences(
customerId: job.customerId,
siteId: job.siteId,
jobId: job.id,
);
}
}
}
static ReportAuditEntry _auditEntry(
DemoReport report,
ReportAuditAction action, {
DateTime? timestamp,
}) {
final occurredAt = timestamp ?? DateTime.now();
return ReportAuditEntry(
id: newUuid(),
reportId: report.id,
action: action,
timestamp: occurredAt,
version: report.version,
snapshot: report,
);
}
Future<void> _enqueue(DemoReport report, SyncAction action) async {
final tenantId = AuthStore.instance.session?.tenantId;
await SyncQueueStore.instance.enqueue(
entityType: 'report',
entityId: report.id,
action: action,
payload: report.toJson(),
tenantId: tenantId,
baseRevision: action == SyncAction.create || tenantId == null
? 0
: SyncQueueStore.instance.reportRevision(tenantId, report.id),
);
await ReportSyncController.instance.synchronize();
await ReportAssetSyncController.instance.synchronize();
}
}
class LocalReportData {
const LocalReportData(this.reports, this.history);
final List<DemoReport> reports;
final List<ReportAuditEntry> history;
}
class LocalReportRepository {
Future<LocalReportData> readAll() async {
final tenantId = AuthStore.instance.session?.tenantId;
final reports =
(await LocalStorage.instance.mobileQuery(
'reports',
where: tenantId == null ? 'tenant_id IS NULL' : 'tenant_id = ?',
whereArgs: tenantId == null ? null : [tenantId],
))
.map(
(row) => DemoReport.fromJson(
jsonDecode(row['payload'] as String) as Map<String, dynamic>,
),
)
.toList();
final history =
(await LocalStorage.instance.mobileQuery(
'report_audit',
where: tenantId == null ? 'tenant_id IS NULL' : 'tenant_id = ?',
whereArgs: tenantId == null ? null : [tenantId],
))
.map(
(row) => ReportAuditEntry.fromJson(
jsonDecode(row['payload'] as String) as Map<String, dynamic>,
),
)
.toList();
return LocalReportData(reports, history);
}
Future<void> replaceAll(
List<DemoReport> reports,
List<ReportAuditEntry> history,
) async {
final tenantId = AuthStore.instance.session?.tenantId;
final where = tenantId == null ? 'tenant_id IS NULL' : 'tenant_id = ?';
final whereArgs = tenantId == null ? <Object?>[] : <Object?>[tenantId];
await LocalStorage.instance.deleteRows(
'reports',
where: where,
whereArgs: whereArgs,
);
await LocalStorage.instance.deleteRows(
'report_audit',
where: where,
whereArgs: whereArgs,
);
for (final report in reports) {
await LocalStorage.instance.replaceRow('reports', {
'id': report.id,
'tenant_id': tenantId,
'customer_id': report.customerId,
'site_id': report.siteId,
'job_id': report.jobId,
'payload': jsonEncode(report.toJson()),
});
}
for (final entry in history) {
await LocalStorage.instance.replaceRow('report_audit', {
'id': entry.id,
'tenant_id': tenantId,
'report_id': entry.reportId,
'timestamp': entry.timestamp.toIso8601String(),
'payload': jsonEncode(entry.toJson()),
});
}
}
}

Architecture

High-level modules detected from the verified public snapshot. Protected implementation details are intentionally omitted.

backendmodule
libmodule
iosmodule
testmodule
androidmodule
docsmodule
webmodule
assetsmodule

Technology stack

DartPHPLOCKPBXPROJSQLMarkdownFlutter / Dart
Dart66.7%
PHP14.5%
LOCK4.0%
PBXPROJ3.8%
SQL3.1%
Markdown1.3%
XML0.9%
YAML0.9%