This commit is contained in:
+113
-7
@@ -1,6 +1,8 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:http_parser/http_parser.dart' show MediaType;
|
||||
|
||||
import '../models/work_day.dart';
|
||||
import '../models/work_interval.dart';
|
||||
import '../models/month_start.dart';
|
||||
@@ -58,7 +60,7 @@ class BookingApi {
|
||||
items.add(WorkDay(
|
||||
date: DateTime(date.year, date.month, date.day),
|
||||
intervals: intervals,
|
||||
targetMinutes: 0, // wird im UI mit Tagesplan/Feiertag ersetzt
|
||||
targetMinutes: 0, // wird im UI ersetzt
|
||||
code: code,
|
||||
));
|
||||
}
|
||||
@@ -85,14 +87,13 @@ class BookingApi {
|
||||
return MonthStart.fromJson(map);
|
||||
}
|
||||
|
||||
/// Einen Tag speichern: date (YYYY-MM-DD), code, come1..leave5 ("HH:mm" oder null/leer)
|
||||
/// Einen Tag speichern
|
||||
Future<void> saveDay(WorkDay day) async {
|
||||
final y = day.date.year.toString().padLeft(4, '0');
|
||||
final m = day.date.month.toString().padLeft(2, '0');
|
||||
final d = day.date.day.toString().padLeft(2, '0');
|
||||
final dateStr = '$y-$m-$d';
|
||||
|
||||
// Intervalle in 5 Slots abbilden
|
||||
final starts = List<String?>.filled(5, null);
|
||||
final ends = List<String?>.filled(5, null);
|
||||
for (int i = 0; i < day.intervals.length && i < 5; i++) {
|
||||
@@ -100,13 +101,12 @@ class BookingApi {
|
||||
ends[i] = fmtTimeOfDay(day.intervals[i].end);
|
||||
}
|
||||
|
||||
// Codes, die Zeiten serverseitig leeren
|
||||
final lockCodes = {'G', 'U', 'SU', 'K'};
|
||||
final payload = <String, dynamic>{
|
||||
'module': 'booking',
|
||||
'function': 'saveDay',
|
||||
'date': dateStr,
|
||||
'code': day.code, // null → wird als NULL gespeichert
|
||||
'code': day.code,
|
||||
'come1': lockCodes.contains(day.code) ? null : starts[0],
|
||||
'leave1': lockCodes.contains(day.code) ? null : ends[0],
|
||||
'come2': lockCodes.contains(day.code) ? null : starts[1],
|
||||
@@ -136,7 +136,6 @@ class BookingApi {
|
||||
}
|
||||
|
||||
/// Startwerte für einen Monat speichern (Upsert auf monthlybooking).
|
||||
/// `monthStart` = 1. des Monats.
|
||||
Future<void> saveMonthStart(
|
||||
DateTime monthStart, {
|
||||
required int starthours,
|
||||
@@ -146,7 +145,7 @@ class BookingApi {
|
||||
}) async {
|
||||
final y = monthStart.year.toString().padLeft(4, '0');
|
||||
final m = monthStart.month.toString().padLeft(2, '0');
|
||||
final d = '01'; // normalize
|
||||
final d = '01';
|
||||
final uri = Uri.parse('https://api.windesign.at/workinghours.php');
|
||||
|
||||
final payload = {
|
||||
@@ -174,4 +173,111 @@ class BookingApi {
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------- PDF (salery / timesheet) --------------------
|
||||
|
||||
Future<bool> hasMonthlyPdf({
|
||||
required String date, // "YYYY-MM-01" (oder YYYY-MM)
|
||||
required String type, // "salery" | "timesheet"
|
||||
}) async {
|
||||
final uri = Uri.parse('https://api.windesign.at/workinghours.php');
|
||||
final res = await client.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'module': 'monthlybooking',
|
||||
'function': 'hasDoc',
|
||||
'date': date,
|
||||
'doctype': type,
|
||||
}),
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('monthlybooking/hasDoc failed: ${res.statusCode} ${res.body}');
|
||||
}
|
||||
final map = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
if (map['error'] == true) {
|
||||
throw Exception('monthlybooking/hasDoc error: ${map['errmsg']}');
|
||||
}
|
||||
return (map['exists'] as bool?) ?? false;
|
||||
}
|
||||
|
||||
Future<Uint8List> getMonthlyPdf({
|
||||
required String date, // "YYYY-MM-01"
|
||||
required String type, // "salery" | "timesheet"
|
||||
}) async {
|
||||
final ymd = date.length == 7 ? '$date-01' : date;
|
||||
final uri = Uri.parse(
|
||||
'https://api.windesign.at/workinghours.php?module=monthlybooking&function=getDoc&date=$ymd&doctype=$type',
|
||||
);
|
||||
final res = await client.get(uri);
|
||||
if (res.statusCode == 200) {
|
||||
return res.bodyBytes;
|
||||
}
|
||||
// Versuch, JSON-Fehler zu lesen
|
||||
try {
|
||||
final map = jsonDecode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>;
|
||||
final msg = map['errmsg'] ?? res.reasonPhrase ?? 'Unknown';
|
||||
throw Exception('getDoc error: $msg');
|
||||
} catch (_) {
|
||||
throw Exception('getDoc failed: ${res.statusCode}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> uploadMonthlyPdf({
|
||||
required String date, // "YYYY-MM-01"
|
||||
required String type, // "salery" | "timesheet"
|
||||
required Uint8List bytes,
|
||||
String filename = 'document.pdf',
|
||||
}) async {
|
||||
final ymd = date.length == 7 ? '$date-01' : date;
|
||||
final uri = Uri.parse(
|
||||
'https://api.windesign.at/workinghours.php?module=monthlybooking&function=saveDoc',
|
||||
);
|
||||
final request = http.MultipartRequest('POST', uri)
|
||||
..fields['date'] = ymd
|
||||
..fields['doctype'] = type;
|
||||
|
||||
request.files.add(
|
||||
http.MultipartFile.fromBytes(
|
||||
'file',
|
||||
bytes,
|
||||
filename: filename,
|
||||
contentType: MediaType('application', 'pdf'),
|
||||
),
|
||||
);
|
||||
|
||||
final streamed = await request.send();
|
||||
final res = await http.Response.fromStream(streamed);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('monthlybooking/saveDoc failed: ${res.statusCode} ${res.body}');
|
||||
}
|
||||
final map = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
if (map['error'] == true) {
|
||||
throw Exception('monthlybooking/saveDoc error: ${map['errmsg']}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteMonthlyPdf({
|
||||
required String date, // "YYYY-MM-01"
|
||||
required String type, // "salery" | "timesheet"
|
||||
}) async {
|
||||
final ymd = date.length == 7 ? '$date-01' : date;
|
||||
final uri = Uri.parse('https://api.windesign.at/workinghours.php');
|
||||
final res = await client.post(
|
||||
uri,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: jsonEncode({
|
||||
'module': 'monthlybooking',
|
||||
'function': 'deleteDoc',
|
||||
'date': ymd,
|
||||
'doctype': type,
|
||||
}),
|
||||
);
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('monthlybooking/deleteDoc failed: ${res.statusCode} ${res.body}');
|
||||
}
|
||||
final map = jsonDecode(res.body) as Map<String, dynamic>;
|
||||
if (map['error'] == true) {
|
||||
throw Exception('monthlybooking/deleteDoc error: ${map['errmsg']}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export 'pdf_platform_stub.dart'
|
||||
if (dart.library.html) 'pdf_platform_web.dart'
|
||||
if (dart.library.io) 'pdf_platform_io.dart';
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:open_filex/open_filex.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<void> platformViewPdf(BuildContext context, Uint8List bytes, {required String filename}) async {
|
||||
final dir = await getTemporaryDirectory();
|
||||
final f = File('${dir.path}/$filename');
|
||||
await f.writeAsBytes(bytes);
|
||||
await OpenFilex.open(f.path);
|
||||
}
|
||||
|
||||
Future<void> platformDownloadPdf(Uint8List bytes, {required String filename}) async {
|
||||
final savePath = await FilePicker.platform.saveFile(
|
||||
dialogTitle: 'PDF speichern als',
|
||||
fileName: filename,
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ['pdf'],
|
||||
lockParentWindow: true,
|
||||
);
|
||||
if (savePath == null) return;
|
||||
final f = File(savePath);
|
||||
await f.writeAsBytes(bytes);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
Future<void> platformViewPdf(BuildContext context, Uint8List bytes, {required String filename}) async {}
|
||||
Future<void> platformDownloadPdf(Uint8List bytes, {required String filename}) async {}
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/material.dart';
|
||||
// ignore: avoid_web_libraries_in_flutter
|
||||
import 'dart:html' as html;
|
||||
|
||||
Future<void> platformViewPdf(BuildContext context, Uint8List bytes, {required String filename}) async {
|
||||
final blob = html.Blob([bytes], 'application/pdf');
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
final viewType = 'pdf-view-${DateTime.now().microsecondsSinceEpoch}';
|
||||
final iframe = html.IFrameElement()
|
||||
..src = url
|
||||
..style.border = '0'
|
||||
..style.width = '100%'
|
||||
..style.height = '100%';
|
||||
// ignore: undefined_prefixed_name
|
||||
ui.platformViewRegistry.registerViewFactory(viewType, (int _) => iframe);
|
||||
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (ctx) => Dialog(
|
||||
insetPadding: const EdgeInsets.all(16),
|
||||
child: SizedBox(
|
||||
width: 1000, height: 700,
|
||||
child: Stack(children: [
|
||||
HtmlElementView(viewType: viewType),
|
||||
Positioned(
|
||||
right: 8, top: 8,
|
||||
child: IconButton(
|
||||
tooltip: 'In neuem Tab öffnen',
|
||||
icon: const Icon(Icons.open_in_new),
|
||||
onPressed: () => html.window.open(url, '_blank'),
|
||||
),
|
||||
),
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
html.Url.revokeObjectUrl(url);
|
||||
}
|
||||
|
||||
Future<void> platformDownloadPdf(Uint8List bytes, {required String filename}) async {
|
||||
final blob = html.Blob([bytes], 'application/pdf');
|
||||
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||
final a = html.AnchorElement(href: url)..download = filename;
|
||||
a.click();
|
||||
html.Url.revokeObjectUrl(url);
|
||||
}
|
||||
+467
-418
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user