Files
timemaster-kiosk/lib/screens/enrollment_screen.dart
T
patrickandClaude Sonnet 5 e42cae74f3 feat: TimeMaster Kiosk Grundgerüst (Login + Stempeln)
Android-Tablet-Kiosk-App, gedacht für Betrieb hinter WireGuard-VPN.
Bearer-Token-Geräte-Auth statt Ed25519-Request-Signing (siehe Backend-
Commit f4ff11a im Haupt-Repo). Ersteinrichtung (Server-URL+Token), PIN/
NFC/Listen-Login, Stempeln Ein/Aus/Pause, Heartbeat-Loop.

Bekannte Einschränkung: Android-NFC (13,56 MHz) ist inkompatibel zu den
125-kHz-EM4100-Karten der bestehenden PC-Terminals – separate Karten nötig,
siehe README.

Vollständiger Plan: /home/sysops/.claude/plans/staged-marinating-dove.md

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dzdm3EWeNdYFUuXFRJryA1
2026-09-10 02:23:59 +02:00

93 lines
3.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import '../app_version.dart';
import '../services/api_client.dart';
import 'login_screen.dart';
/// Einmalige Ersteinrichtung: Server-URL + Bearer-Token eintragen, die per
/// `timemaster kiosk add --auth-mode bearer` auf dem Server generiert wurden.
/// Kein Keypair, kein QR-Scan – bewusst simpel (VPN übernimmt Transport-
/// Sicherheit, siehe Plan Android-Tablet-Kiosk 2026-09-10).
class EnrollmentScreen extends StatefulWidget {
const EnrollmentScreen({super.key});
@override
State<EnrollmentScreen> createState() => _EnrollmentScreenState();
}
class _EnrollmentScreenState extends State<EnrollmentScreen> {
final _urlCtrl = TextEditingController(text: ApiClient.instance.baseUrl);
final _tokenCtrl = TextEditingController();
bool _saving = false;
String? _error;
Future<void> _save() async {
if (_urlCtrl.text.trim().isEmpty || _tokenCtrl.text.trim().isEmpty) {
setState(() => _error = 'Server-URL und Token sind Pflichtfelder.');
return;
}
setState(() {
_saving = true;
_error = null;
});
await ApiClient.instance.enroll(
baseUrl: _urlCtrl.text.trim(),
deviceToken: _tokenCtrl.text.trim(),
);
if (!mounted) return;
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (_) => const LoginScreen()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('TimeMaster Kiosk · v$kAppVersion')),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.tablet_mac, size: 64, color: Colors.indigo),
const SizedBox(height: 16),
Text('Ersteinrichtung', style: Theme.of(context).textTheme.headlineSmall),
const SizedBox(height: 8),
const Text(
'Bearer-Token vom Admin einfügen (per "timemaster kiosk add --auth-mode bearer" erzeugt).',
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
TextField(
controller: _urlCtrl,
decoration: const InputDecoration(labelText: 'Server-URL', hintText: 'https://.../api/v1'),
),
const SizedBox(height: 12),
TextField(
controller: _tokenCtrl,
decoration: const InputDecoration(labelText: 'Bearer-Token'),
obscureText: true,
),
const SizedBox(height: 24),
if (_error != null) ...[
Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 12),
],
SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _saving ? null : _save,
child: const Text('Einrichten'),
),
),
],
),
),
),
),
);
}
}