diff --git a/.fvmrc b/.fvmrc
index 5c7f58e2b..0fdcb4876 100644
--- a/.fvmrc
+++ b/.fvmrc
@@ -1,3 +1,3 @@
{
- "flutter": "3.24.5"
+ "flutter": "3.27.1"
}
\ No newline at end of file
diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml
index 6d45cb686..43575a1f0 100644
--- a/.github/workflows/web.yml
+++ b/.github/workflows/web.yml
@@ -15,7 +15,7 @@ jobs:
- name: Flutter
uses: subosito/flutter-action@v2
with:
- flutter-version: '3.24.5'
+ flutter-version: '3.27.1'
channel: 'stable'
- run: flutter build web --web-renderer canvaskit --release --base-href /
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0deca682e..b01430d59 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
{
- "dart.flutterSdkPath": ".fvm/versions/3.24.5"
+ "dart.flutterSdkPath": ".fvm/versions/3.27.1"
}
\ No newline at end of file
diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 000000000..55aebf98a
--- /dev/null
+++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/assets/translations/cs.json b/assets/translations/cs.json
index 551e835ce..bf6b1d8d6 100644
--- a/assets/translations/cs.json
+++ b/assets/translations/cs.json
@@ -349,5 +349,7 @@
"Tickets": "Vstupenky",
"Orders": "Objednávky",
"State": "Stav",
+ "Do you want to send the tickets to orders?": "Chcete poslat vstupenky pro objednávky?",
+ "Send tickets": "Poslat vstupenky",
"_": "_"
}
\ No newline at end of file
diff --git a/assets/translations/en.json b/assets/translations/en.json
index c82be63e3..b6dec2f2f 100644
--- a/assets/translations/en.json
+++ b/assets/translations/en.json
@@ -349,5 +349,7 @@
"Tickets": "Tickets",
"Orders": "Orders",
"State": "State",
+ "Do you want to send the tickets to orders?": "Do you want to send the tickets to orders?",
+ "Send tickets": "Send tickets",
"_":"_"
}
\ No newline at end of file
diff --git a/lib/AppRouter.dart b/lib/AppRouter.dart
index 6935c4ce5..6e6c416da 100644
--- a/lib/AppRouter.dart
+++ b/lib/AppRouter.dart
@@ -43,8 +43,8 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: InstallRoute.page, path: sl(InstallPage.ROUTE)),
AutoRoute(page: AdminDashboardRoute.page, path: sl(AdminDashboardPage.ROUTE)),
AutoRoute(page: AdminDashboardRoute.page, path: sl(AdminDashboardPage.ROUTE)),
- AutoRoute(page: FormRoute.page, path: "/${FormPage.ROUTE}/:id"),
- AutoRoute(page: FormEditRoute.page, path: "/${FormPage.ROUTE}/:formKey/edit"),
+ AutoRoute(page: FormRoute.page, path: "/${FormPage.ROUTE}/:formLink"),
+ AutoRoute(page: FormEditRoute.page, path: "/${FormPage.ROUTE}/:formLink/edit"),
AutoRoute(page: CheckRoute.page, path: "/:{$LINK}/${CheckPage.ROUTE}/:id"),
AutoRoute(page: NewsFormRoute.page, path: "/:{$LINK}/${NewsFormPage.ROUTE}"),
AutoRoute(page: HtmlEditorRoute.page, path: "/:{$LINK}/${HtmlEditorPage.ROUTE}"),
diff --git a/lib/AppRouter.gr.dart b/lib/AppRouter.gr.dart
index 15a77a293..cdc451918 100644
--- a/lib/AppRouter.gr.dart
+++ b/lib/AppRouter.gr.dart
@@ -268,15 +268,15 @@ class ForgotPasswordRoute extends _i28.PageRouteInfo {
class FormEditRoute extends _i28.PageRouteInfo {
FormEditRoute({
_i29.Key? key,
- String? formKey,
+ String? formLink,
List<_i28.PageRouteInfo>? children,
}) : super(
FormEditRoute.name,
args: FormEditRouteArgs(
key: key,
- formKey: formKey,
+ formLink: formLink,
),
- rawPathParams: {'formKey': formKey},
+ rawPathParams: {'formLink': formLink},
initialChildren: children,
);
@@ -288,12 +288,12 @@ class FormEditRoute extends _i28.PageRouteInfo {
final pathParams = data.inheritedPathParams;
final args = data.argsAs(
orElse: () =>
- FormEditRouteArgs(formKey: pathParams.optString('formKey')));
+ FormEditRouteArgs(formLink: pathParams.optString('formLink')));
return _i28.DeferredWidget(
_i7.loadLibrary,
() => _i7.FormEditPage(
key: args.key,
- formKey: args.formKey,
+ formLink: args.formLink,
),
);
},
@@ -303,16 +303,16 @@ class FormEditRoute extends _i28.PageRouteInfo {
class FormEditRouteArgs {
const FormEditRouteArgs({
this.key,
- this.formKey,
+ this.formLink,
});
final _i29.Key? key;
- final String? formKey;
+ final String? formLink;
@override
String toString() {
- return 'FormEditRouteArgs{key: $key, formKey: $formKey}';
+ return 'FormEditRouteArgs{key: $key, formLink: $formLink}';
}
}
@@ -343,10 +343,15 @@ class FormEditorTab extends _i28.PageRouteInfo {
class FormRoute extends _i28.PageRouteInfo {
FormRoute({
_i29.Key? key,
+ String? formLink,
List<_i28.PageRouteInfo>? children,
}) : super(
FormRoute.name,
- args: FormRouteArgs(key: key),
+ args: FormRouteArgs(
+ key: key,
+ formLink: formLink,
+ ),
+ rawPathParams: {'formLink': formLink},
initialChildren: children,
);
@@ -355,24 +360,34 @@ class FormRoute extends _i28.PageRouteInfo {
static _i28.PageInfo page = _i28.PageInfo(
name,
builder: (data) {
- final args =
- data.argsAs(orElse: () => const FormRouteArgs());
+ final pathParams = data.inheritedPathParams;
+ final args = data.argsAs(
+ orElse: () =>
+ FormRouteArgs(formLink: pathParams.optString('formLink')));
return _i28.DeferredWidget(
_i9.loadLibrary,
- () => _i9.FormPage(key: args.key),
+ () => _i9.FormPage(
+ key: args.key,
+ formLink: args.formLink,
+ ),
);
},
);
}
class FormRouteArgs {
- const FormRouteArgs({this.key});
+ const FormRouteArgs({
+ this.key,
+ this.formLink,
+ });
final _i29.Key? key;
+ final String? formLink;
+
@override
String toString() {
- return 'FormRouteArgs{key: $key}';
+ return 'FormRouteArgs{key: $key, formLink: $formLink}';
}
}
diff --git a/lib/dataModelsEshop/ProductModel.dart b/lib/dataModelsEshop/ProductModel.dart
index 535dea041..d8a3795b1 100644
--- a/lib/dataModelsEshop/ProductModel.dart
+++ b/lib/dataModelsEshop/ProductModel.dart
@@ -12,6 +12,7 @@ class ProductModel {
int? productType;
int? occasion;
String? productTypeString;
+ int? order;
static const String foodType = "food";
static const String taxiType = "taxi";
@@ -20,17 +21,18 @@ class ProductModel {
factory ProductModel.fromJson(Map json) {
return ProductModel(
- id: json[TbEshop.products.id],
- createdAt: json[TbEshop.products.created_at] != null ? DateTime.parse(json[TbEshop.products.created_at]) : null,
- updatedAt: json[TbEshop.products.updated_at] != null ? DateTime.parse(json[TbEshop.products.updated_at]) : null,
- title: json[TbEshop.products.title],
- isHidden: json[TbEshop.products.is_hidden],
- description: json[TbEshop.products.description],
- price: json[TbEshop.products.price] != null ? double.tryParse(json[TbEshop.products.price].toString()) : null,
- data: json[TbEshop.products.data],
- productType: json[TbEshop.products.product_type],
- occasion: json[TbEshop.products.occasion],
- productTypeString: json[metaTypeField],
+ id: json[TbEshop.products.id],
+ createdAt: json[TbEshop.products.created_at] != null ? DateTime.parse(json[TbEshop.products.created_at]) : null,
+ updatedAt: json[TbEshop.products.updated_at] != null ? DateTime.parse(json[TbEshop.products.updated_at]) : null,
+ title: json[TbEshop.products.title],
+ isHidden: json[TbEshop.products.is_hidden],
+ description: json[TbEshop.products.description],
+ price: json[TbEshop.products.price] != null ? double.tryParse(json[TbEshop.products.price].toString()) : null,
+ data: json[TbEshop.products.data],
+ productType: json[TbEshop.products.product_type],
+ occasion: json[TbEshop.products.occasion],
+ productTypeString: json[metaTypeField],
+ order: json[TbEshop.products.order] // Adding order to the JSON factory method
);
}
@@ -45,6 +47,7 @@ class ProductModel {
TbEshop.products.data: data,
TbEshop.products.product_type: productType,
TbEshop.products.occasion: occasion,
+ 'order': order
};
String toBasicString() => title ?? id.toString();
@@ -61,5 +64,6 @@ class ProductModel {
this.productType,
this.occasion,
this.productTypeString,
+ this.order,
});
}
diff --git a/lib/dataModelsEshop/TbEshop.dart b/lib/dataModelsEshop/TbEshop.dart
index 11a88852e..656278b98 100644
--- a/lib/dataModelsEshop/TbEshop.dart
+++ b/lib/dataModelsEshop/TbEshop.dart
@@ -35,6 +35,7 @@ class ProductsTb {
String get data => "data";
String get product_type => "product_type";
String get occasion => "occasion";
+ String get order => "order";
}
class OrderProductTicketTb {
diff --git a/lib/dataModelsEshop/TicketModel.dart b/lib/dataModelsEshop/TicketModel.dart
index d66f47795..17147767a 100644
--- a/lib/dataModelsEshop/TicketModel.dart
+++ b/lib/dataModelsEshop/TicketModel.dart
@@ -20,7 +20,7 @@ class TicketModel extends IPlutoRowModel {
double? totalPrice;
// Relating spots and products directly to the ticket
- List? relatedSpots;
+ BlueprintObjectModel? relatedSpot;
List? relatedProducts;
// Relating order directly to the ticket
@@ -34,6 +34,7 @@ class TicketModel extends IPlutoRowModel {
static const String metaRelatedOrder = "related_order";
static const String metaTicketsProducts = "ticket_products";
static const String metaPrice = "price";
+ static const String metaSpot = "spot";
TicketModel({
this.id,
@@ -44,7 +45,7 @@ class TicketModel extends IPlutoRowModel {
this.occasion,
this.note,
this.noteHidden,
- this.relatedSpots,
+ this.relatedSpot,
this.relatedProducts,
this.relatedOrder,
});
@@ -95,6 +96,10 @@ class TicketModel extends IPlutoRowModel {
value: relatedProducts != null
? relatedProducts!.map((p)=>p.toBasicString()).join(" | ")
: ""),
+ metaSpot: PlutoCell(
+ value: relatedSpot != null
+ ? relatedSpot?.toShortString()
+ : ""),
metaPrice: PlutoCell(value: totalPrice != null ? Utilities.formatPrice(context, totalPrice!) : ""),
});
}
diff --git a/lib/dataServices/DbEshop.dart b/lib/dataServices/DbEshop.dart
index 7fa7fa8c7..3dd51c7a0 100644
--- a/lib/dataServices/DbEshop.dart
+++ b/lib/dataServices/DbEshop.dart
@@ -28,7 +28,7 @@ class DbEshop {
"${TbEshop.product_types.id},"
"${TbEshop.product_types.type},"
"${TbEshop.product_types.title},"
- "${TbEshop.products.table}(${TbEshop.products.id},${TbEshop.products.title},${TbEshop.products.price})"
+ "${TbEshop.products.table}(${TbEshop.products.id},${TbEshop.products.title},${TbEshop.products.price},${TbEshop.products.order})"
)
.eq(TbEshop.product_types.occasion, currentOccasion)
.eq("${TbEshop.products.table}.${TbEshop.products.is_hidden}", false);
@@ -37,7 +37,7 @@ class DbEshop {
data.map((x) {
var toReturn = ProductTypeModel.fromJson(x);
toReturn.products = toReturn.products?.sortedBy((i) => i.title ?? "");
- toReturn.products = toReturn.products?.sortedBy((i) => i.price ?? 0);
+ toReturn.products = toReturn.products?.sortedBy((i) => i.order ?? 0);
for (ProductModel v in toReturn.products??[]){
v.title = v.price != null && v.price! > 0 ? "${v.title} (${Utilities.formatPrice(context, v.price!)})" : v.title;
}
@@ -51,9 +51,9 @@ class DbEshop {
return await _supabase.functions.invoke("send-ticket-order", body: {"orderDetails": data});
}
- static Future getForm(String formKey) async {
+ static Future getFormFromLink(String link) async {
final response = await _supabase
- .rpc('get_form', params: {'form_key': formKey});
+ .rpc('get_form_from_link', params: {'form_link': link});
if(response["code"] == 200){
var form = FormModel.fromJson(response["data"]);
@@ -62,11 +62,11 @@ class DbEshop {
return null;
}
- static Future getFormForEdit(String formKey) async {
+ static Future getFormForEdit(String formLink) async {
var data = await _supabase
.from(Tb.forms.table)
.select()
- .eq(Tb.forms.key, formKey)
+ .eq(Tb.forms.link, formLink)
.maybeSingle();
if(data==null)
{
@@ -125,7 +125,7 @@ class DbEshop {
final response = await _supabase.rpc(
'get_blueprint_editor',
params: {
- 'form_key': formKey,
+ 'form_link': formKey,
},
);
@@ -176,12 +176,12 @@ class DbEshop {
return true;
}
- static Future> getAllOrders(String formKey) async {
+ static Future> getAllOrders(String formLink) async {
final response = await _supabase.rpc(
'get_orders',
params: {
- 'form_key': formKey,
+ 'form_link': formLink,
},
);
@@ -206,9 +206,9 @@ class DbEshop {
for (var ticket in order.relatedTickets!) {
// Relate spots to the ticket via orderProductTickets
- ticket.relatedSpots = spots!.where((spot) {
+ ticket.relatedSpot = spots!.firstWhereOrNull((spot) {
return orderProductTickets!.any((opt) => opt.ticketId == ticket.id && opt.id == spot.orderProductTicket);
- }).toList();
+ });
// Relate products to the ticket via orderProductTickets
ticket.relatedProducts = products!.where((product) {
@@ -244,8 +244,8 @@ class DbEshop {
return orders.sortedBy((ou)=>ou.createdAt!).reversed.toList();
}
- static Future> getAllTickets(String formKey) async {
- var orders = await getAllOrders(formKey);
+ static Future> getAllTickets(String formLink) async {
+ var orders = await getAllOrders(formLink);
List toReturn = [];
for(var o in orders){
@@ -266,6 +266,31 @@ class DbEshop {
toReturn = toReturn.sortedBy((ou)=>ou.createdAt!).reversed.toList();
return toReturn;
}
+
+ static Future sendTicketsToEmail({
+ required List ticketIds,
+ required String email,
+ required int oc, // Occasion ID
+ }) async {
+ final body = {
+ "ticketIds": ticketIds,
+ "email": email,
+ "oc": oc,
+ };
+
+ try {
+ final response = await _supabase.functions.invoke(
+ "send-tickets",
+ body: body,
+ );
+
+ return response;
+ } catch (e) {
+ print("Unexpected error in sendTicketsToEmail: $e");
+ rethrow;
+ }
+ }
+
static Future deleteOrder(OrderModel model) async {
}
diff --git a/lib/pages/Eshop/BlueprintEditorTab.dart b/lib/pages/Eshop/BlueprintEditorTab.dart
index af02e2cec..4905ff6cf 100644
--- a/lib/pages/Eshop/BlueprintEditorTab.dart
+++ b/lib/pages/Eshop/BlueprintEditorTab.dart
@@ -32,7 +32,7 @@ class BlueprintTab extends StatefulWidget {
class _BlueprintTabState extends State {
BlueprintModel? blueprint;
BlueprintGroupModel? currentGroup;
- String? formKey;
+ String? formLink;
List allBoxes = [];
@@ -44,8 +44,8 @@ class _BlueprintTabState extends State {
@override
void didChangeDependencies() {
super.didChangeDependencies();
- if (formKey == null && context.routeData.pathParams.isNotEmpty) {
- formKey = context.routeData.pathParams.getString("formKey");
+ if (formLink == null && context.routeData.pathParams.isNotEmpty) {
+ formLink = context.routeData.pathParams.getString("formLink");
}
loadData();
}
@@ -631,7 +631,7 @@ class _BlueprintTabState extends State {
}
Future loadData() async {
- blueprint = await DbEshop.getBlueprintForEdit(formKey!);
+ blueprint = await DbEshop.getBlueprintForEdit(formLink!);
setState(() {});
}
}
diff --git a/lib/pages/Eshop/EshopColumns.dart b/lib/pages/Eshop/EshopColumns.dart
index b7370e329..c9b8390a7 100644
--- a/lib/pages/Eshop/EshopColumns.dart
+++ b/lib/pages/Eshop/EshopColumns.dart
@@ -15,6 +15,7 @@ class EshopColumns {
static const String TICKET_TOTAL_PRICE = "ticketTotalPrice";
static const String TICKET_PRODUCTS = "ticketProducts";
static const String TICKET_CREATED_AT = "ticketCreatedAt";
+ static const String TICKET_SPOT = "ticketSpot";
static const String ORDER_ID = "orderId";
static const String ORDER_PRICE = "orderPrice";
@@ -124,6 +125,16 @@ class EshopColumns {
width: 200,
),
],
+ TICKET_SPOT: [
+ PlutoColumn(
+ readOnly: true,
+ enableEditingMode: false,
+ title: "Spot".tr(),
+ field: TicketModel.metaSpot,
+ type: PlutoColumnType.text(),
+ width: 60,
+ ),
+ ],
ORDER_ID: [
PlutoColumn(
hide: true,
diff --git a/lib/pages/Eshop/FormEditPage.dart b/lib/pages/Eshop/FormEditPage.dart
index 2fe1b3da3..b073791a2 100644
--- a/lib/pages/Eshop/FormEditPage.dart
+++ b/lib/pages/Eshop/FormEditPage.dart
@@ -1,14 +1,18 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
+import 'package:fstapp/RouterService.dart';
import 'package:fstapp/components/dataGrid/AdminPageHelper.dart';
import 'package:easy_localization/easy_localization.dart';
+import 'package:fstapp/dataServices/AuthService.dart';
+import 'package:fstapp/dataServices/RightsService.dart';
+import 'package:fstapp/pages/LoginPage.dart';
@RoutePage()
class FormEditPage extends StatefulWidget {
static const ROUTE = "formEdit";
- String? formKey;
+ String? formLink;
- FormEditPage({super.key, @pathParam this.formKey});
+ FormEditPage({super.key, @pathParam this.formLink});
@override
_FormEditPageState createState() => _FormEditPageState();
@@ -25,6 +29,15 @@ class _FormEditPageState extends State with SingleTickerProviderSt
AdminTabDefinition.tickets,
];
+ @override
+ Future didChangeDependencies() async {
+ if (!AuthService.isLoggedIn()) {
+ await RouterService.navigate(context, LoginPage.ROUTE);
+ }
+
+ super.didChangeDependencies();
+ }
+
@override
void initState() {
super.initState();
diff --git a/lib/pages/Eshop/FormEditorTab.dart b/lib/pages/Eshop/FormEditorTab.dart
index a8ec84c75..59bc677ad 100644
--- a/lib/pages/Eshop/FormEditorTab.dart
+++ b/lib/pages/Eshop/FormEditorTab.dart
@@ -22,18 +22,18 @@ class FormEditorTab extends StatefulWidget {
class _FormEditorTabState extends State {
FormModel? form;
- String? formKey;
+ String? formLink;
@override
void didChangeDependencies() {
super.didChangeDependencies();
- if (formKey == null && context.routeData.pathParams.isNotEmpty) {
- formKey = context.routeData.pathParams.getString("formKey");
+ if (formLink == null && context.routeData.pathParams.isNotEmpty) {
+ formLink = context.routeData.pathParams.getString("formLink");
}
loadData();
}
Future loadData() async {
- form = await DbEshop.getFormForEdit(formKey!);
+ form = await DbEshop.getFormForEdit(formLink!);
setState(() {});
}
diff --git a/lib/pages/Eshop/FormPage.dart b/lib/pages/Eshop/FormPage.dart
index 14d22ea2c..2ad578484 100644
--- a/lib/pages/Eshop/FormPage.dart
+++ b/lib/pages/Eshop/FormPage.dart
@@ -1,5 +1,4 @@
import 'dart:async';
-import 'dart:convert';
import 'package:auto_route/auto_route.dart';
import 'package:collection/collection.dart';
@@ -31,8 +30,8 @@ import 'package:fstapp/widgets/SeatReservationWidget.dart';
class FormPage extends StatefulWidget {
static const ROUTE = "form";
- String? formKey;
- FormPage({super.key});
+ String? formLink;
+ FormPage({super.key, @pathParam this.formLink});
@override
_FormPageState createState() => _FormPageState();
@@ -52,8 +51,8 @@ class _FormPageState extends State {
@override
Future didChangeDependencies() async {
- if (widget.formKey == null && context.routeData.hasPendingChildren) {
- widget.formKey = context.routeData.pendingChildren[0].pathParams.getString("formKey");
+ if (widget.formLink == null && context.routeData.hasPendingChildren) {
+ widget.formLink = context.routeData.pendingChildren[0].pathParams.getString("formLink");
}
await loadData();
@@ -321,7 +320,7 @@ class _FormPageState extends State {
onPressed: () {
RouterService.navigate(
context,
- "${FormPage.ROUTE}/${form!.formKey}/edit")
+ "${FormPage.ROUTE}/${widget.formLink}/edit")
.then((value) => loadData());
},
child: const Icon(Icons.edit),
@@ -364,12 +363,12 @@ class _FormPageState extends State {
_isLoading = true;
});
- // if(widget.id == null) {
- // return;
- // }
+ if(widget.formLink == null) {
+ return;
+ }
//var key = UuidConverter.base62ToUuid(widget.id!);
- form = await DbEshop.getForm("7f4e3892-a544-4385-b933-61117e9755c3");
+ form = await DbEshop.getFormFromLink(widget.formLink!);
if (form == null) {
return;
}
diff --git a/lib/pages/Eshop/OrdersTab.dart b/lib/pages/Eshop/OrdersTab.dart
index 2105aae64..7c1f2bb97 100644
--- a/lib/pages/Eshop/OrdersTab.dart
+++ b/lib/pages/Eshop/OrdersTab.dart
@@ -1,3 +1,4 @@
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fstapp/components/dataGrid/DataGridAction.dart';
import 'package:fstapp/components/dataGrid/SingleTableDataGrid.dart';
@@ -7,6 +8,8 @@ import 'package:fstapp/dataServices/DbEshop.dart';
import 'package:fstapp/dataServices/RightsService.dart';
import 'package:fstapp/pages/Eshop/EshopColumns.dart';
import 'package:auto_route/auto_route.dart';
+import 'package:fstapp/services/DialogHelper.dart';
+import 'package:fstapp/services/ToastHelper.dart';
class OrdersTab extends StatefulWidget {
const OrdersTab({super.key});
@@ -16,16 +19,24 @@ class OrdersTab extends StatefulWidget {
}
class _OrdersTabState extends State {
- String? formKey;
+ String? formLink;
+ Key refreshKey = UniqueKey();
+
@override
void didChangeDependencies() {
super.didChangeDependencies();
- if (formKey == null && context.routeData.pathParams.isNotEmpty) {
- formKey = context.routeData.pathParams.getString("formKey");
+ if (formLink == null && context.routeData.pathParams.isNotEmpty) {
+ formLink = context.routeData.pathParams.getString("formLink");
}
}
+ Future refreshData() async {
+ setState(() {
+ refreshKey = UniqueKey();
+ });
+ }
+
static const List columnIdentifiers = [
EshopColumns.ORDER_ID,
EshopColumns.ORDER_SYMBOL,
@@ -41,16 +52,71 @@ class _OrdersTabState extends State {
@override
Widget build(BuildContext context) {
- return SingleTableDataGrid(
+ return KeyedSubtree(child: SingleTableDataGrid(
context,
- () => DbEshop.getAllOrders(formKey!),
+ () => DbEshop.getAllOrders(formLink!),
OrderModel.fromPlutoJson,
DataGridFirstColumn.check,
TbEshop.orders.id,
actionsExtended: DataGridActionsController(
areAllActionsEnabled: RightsService.canUpdateUsers,
isAddActionPossible: () => false),
+ headerChildren: [
+ DataGridAction(
+ name: "Send tickets".tr(),
+ action: (SingleTableDataGrid dataGrid, [_]) => sendTickets(dataGrid),
+ isEnabled: RightsService.isEditor,
+ ),
+ ],
columns: EshopColumns.generateColumns(columnIdentifiers),
- ).DataGrid();
+ ).DataGrid());
+ }
+
+ Future sendTickets(SingleTableDataGrid dataGrid) async {
+ var selectedTickets = _getChecked(dataGrid);
+
+ if (selectedTickets.isEmpty) {
+ return;
+ }
+
+ var confirm = await DialogHelper.showConfirmationDialogAsync(
+ context,
+ "Storno".tr(),
+ "${"Do you want to send the tickets to orders?".tr()} (${selectedTickets.length})",
+ );
+
+ if (confirm) {
+ var allOrders = await DbEshop.getAllOrders(formLink!);
+ var stornoFutures = selectedTickets.map((order) {
+ return () async {
+ var o = allOrders.firstWhere((o)=>o.id == order.id);
+ await sendTicketsToEmail(o);
+ };
+ }).toList();
+
+ await DialogHelper.showProgressDialogAsync(
+ context,
+ "Processing".tr(),
+ stornoFutures.length,
+ futures: stornoFutures,
+ );
+ refreshData();
+ }
+ }
+
+ Future sendTicketsToEmail(OrderModel order) async {
+ await DbEshop.sendTicketsToEmail(
+ ticketIds: order.relatedTickets!.map((t)=>t.id!).toList(),
+ email: order.data!["email"],
+ oc: RightsService.currentOccasion!,
+ );
+ }
+
+ List _getChecked(SingleTableDataGrid dataGrid) {
+ return List.from(
+ dataGrid.stateManager.refRows.originalList
+ .where((row) => row.checked == true)
+ .map((row) => OrderModel.fromPlutoJson(row.toJson())),
+ );
}
}
\ No newline at end of file
diff --git a/lib/pages/Eshop/TicketsTab.dart b/lib/pages/Eshop/TicketsTab.dart
index 12d267669..61f01a533 100644
--- a/lib/pages/Eshop/TicketsTab.dart
+++ b/lib/pages/Eshop/TicketsTab.dart
@@ -19,15 +19,15 @@ class TicketsTab extends StatefulWidget {
}
class _TicketsTabState extends State {
- String? formKey;
+ String? formLink;
Key refreshKey = UniqueKey();
@override
void didChangeDependencies() {
super.didChangeDependencies();
- if (formKey == null && context.routeData.pathParams.isNotEmpty) {
- formKey = context.routeData.pathParams.getString("formKey");
+ if (formLink == null && context.routeData.pathParams.isNotEmpty) {
+ formLink = context.routeData.pathParams.getString("formLink");
}
}
@@ -45,6 +45,7 @@ class _TicketsTabState extends State {
EshopColumns.TICKET_CREATED_AT,
EshopColumns.TICKET_STATE,
EshopColumns.TICKET_TOTAL_PRICE,
+ EshopColumns.TICKET_SPOT,
EshopColumns.TICKET_PRODUCTS,
EshopColumns.TICKET_NOTE,
EshopColumns.TICKET_NOTE_HIDDEN,
@@ -57,7 +58,7 @@ class _TicketsTabState extends State {
child:
SingleTableDataGrid(
context,
- () => DbEshop.getAllTickets(formKey!),
+ () => DbEshop.getAllTickets(formLink!),
TicketModel.fromPlutoJson,
DataGridFirstColumn.check,
TbEshop.tickets.id,
diff --git a/netlify.toml b/netlify.toml
index bf18f80f5..1d957d29e 100644
--- a/netlify.toml
+++ b/netlify.toml
@@ -1,3 +1,3 @@
[build]
-command = "curl -L https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.24.5-stable.tar.xz | tar -xJf - -C /opt/buildhome && PATH=/opt/buildhome/flutter/bin:$PATH flutter precache && PATH=/opt/buildhome/flutter/bin:$PATH flutter build web --web-renderer canvaskit --release"
+command = "curl -L https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.27.1-stable.tar.xz | tar -xJf - -C /opt/buildhome && PATH=/opt/buildhome/flutter/bin:$PATH flutter precache && PATH=/opt/buildhome/flutter/bin:$PATH flutter build web --web-renderer canvaskit --release"
publish = "build/web"
\ No newline at end of file
diff --git a/pubspec.yaml b/pubspec.yaml
index 18a1eac89..a471f5a5d 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -32,7 +32,7 @@ dependencies:
sdk: flutter
flutter:
sdk: flutter
- flutter_svg: ^2.0.16
+ flutter_svg: ^2.0.17
fluttertoast: ^8.2.10
# The following adds the Cupertino Icons font to your application.
@@ -43,7 +43,7 @@ dependencies:
flutter_map_marker_popup: ^7.0.0
# flutter_map_marker_popup:
# path: ./pckgs/flutter_map_marker_popup-master
- supabase_flutter: ^2.8.2
+ supabase_flutter: ^2.8.3
flutter_secure_storage: ^9.2.2
timelines_plus: ^1.0.4
intl: ^0.19.0
@@ -62,15 +62,15 @@ dependencies:
sembast_web: ^2.4.0+4
path_provider: ^2.1.5
select_dialog: ^2.0.1
- onesignal_flutter: ^5.2.7
+ onesignal_flutter: ^5.2.9
package_info_plus: ^8.1.2
flutter_animate: ^4.5.2
easy_localization: ^3.0.7
flutter_map_cancellable_tile_provider: ^3.0.2
simple_shadow: ^0.3.1
pwa_install: ^0.0.5
- flutter_form_builder: ^9.5.0
- form_builder_validators: ^11.0.0
+ flutter_form_builder: ^9.7.0
+ form_builder_validators: ^11.1.1
qr_flutter: ^4.1.0
screenshot: ^3.0.0
image_downloader_web: ^2.0.4
@@ -78,7 +78,7 @@ dependencies:
flutter_map_pmtiles: ^1.0.4
flutter_map_animations: ^0.8.0
auto_route: ^9.2.2
- adaptive_theme: ^3.6.0
+ adaptive_theme: ^3.7.0
connectivity_plus: ^6.1.1
dev_dependencies:
diff --git a/scripts/database/eshop/get_blueprint_editor.sql b/scripts/database/eshop/get_blueprint_editor.sql
index 1904a6856..94d71f588 100644
--- a/scripts/database/eshop/get_blueprint_editor.sql
+++ b/scripts/database/eshop/get_blueprint_editor.sql
@@ -1,5 +1,5 @@
CREATE OR REPLACE FUNCTION get_blueprint_editor(
- form_key UUID
+ form_link TEXT
)
RETURNS JSONB
LANGUAGE plpgsql
@@ -16,13 +16,13 @@ DECLARE
valid_spots JSONB;
occasion_id BIGINT;
BEGIN
- -- Resolve blueprint_id using form_key
+ -- Resolve blueprint_id using form_link
SELECT blueprint
INTO blueprint_id
FROM public.forms
- WHERE key = form_key;
+ WHERE link = form_link;
- -- Check if form_key is valid
+ -- Check if form_link is valid
IF blueprint_id IS NULL THEN
RETURN jsonb_build_object('code', 404, 'message', 'Form key does not exist or is not linked to a blueprint');
END IF;
diff --git a/scripts/database/eshop/get_form_from_link.sql b/scripts/database/eshop/get_form_from_link.sql
new file mode 100644
index 000000000..02046cc9f
--- /dev/null
+++ b/scripts/database/eshop/get_form_from_link.sql
@@ -0,0 +1,46 @@
+CREATE OR REPLACE FUNCTION get_form_from_link(form_link TEXT)
+RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$
+DECLARE
+ allData JSON;
+ generated_secret UUID := gen_random_uuid();
+BEGIN
+ -- Check if the form is open based on the provided link
+ IF NOT EXISTS (
+ SELECT 1
+ FROM public.forms
+ WHERE link = form_link AND is_open = true
+ ) THEN
+ -- Return an error if the form is not open
+ RETURN jsonb_build_object(
+ 'code', 400,
+ 'message', 'Form is not open.'
+ );
+ END IF;
+
+ -- Build the JSON response with form data if the form is open
+ SELECT jsonb_build_object(
+ 'code', 200,
+ 'data', jsonb_build_object(
+ 'id', f.id,
+ 'key', f.key,
+ 'created_at', f.created_at,
+ 'data', f.data,
+ 'type', f.type,
+ 'header', f.header,
+ 'footer', f.footer,
+ 'occasion', f.occasion,
+ 'blueprint', f.blueprint,
+ 'deadline_duration_seconds', f.deadline_duration_seconds,
+ 'account_number', ba.account_number,
+ 'secret', generated_secret
+ )
+ )
+ INTO allData
+ FROM public.forms f
+ LEFT JOIN eshop.bank_accounts ba ON f.bank_account = ba.id
+ WHERE f.link = form_link;
+
+ -- Return the constructed JSON data
+ RETURN allData;
+END;
+$$;
\ No newline at end of file
diff --git a/scripts/database/eshop/get_orders.sql b/scripts/database/eshop/get_orders.sql
index d9eba7a77..d4c045ae5 100644
--- a/scripts/database/eshop/get_orders.sql
+++ b/scripts/database/eshop/get_orders.sql
@@ -1,5 +1,5 @@
CREATE OR REPLACE FUNCTION get_orders(
- form_key UUID
+ form_link TEXT
)
RETURNS JSONB
LANGUAGE plpgsql
@@ -15,13 +15,13 @@ DECLARE
orderProductTicketsData JSONB;
paymentInfoData JSONB;
BEGIN
- -- Resolve blueprint_id using form_key
+ -- Resolve blueprint_id using form_link
SELECT blueprint
INTO blueprint_id
FROM public.forms
- WHERE key = form_key;
+ WHERE link = form_link;
- -- Check if form_key is valid
+ -- Check if form_link is valid
IF blueprint_id IS NULL THEN
RETURN jsonb_build_object('code', 404, 'message', 'Form key does not exist or is not linked to a blueprint');
END IF;
diff --git a/scripts/database/eshop/get_tickets_with_details.sql b/scripts/database/eshop/get_tickets_with_details.sql
new file mode 100644
index 000000000..b127a5f1c
--- /dev/null
+++ b/scripts/database/eshop/get_tickets_with_details.sql
@@ -0,0 +1,32 @@
+CREATE OR REPLACE FUNCTION get_tickets_with_details(ticket_ids BIGINT[])
+RETURNS JSONB
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ tickets JSONB;
+BEGIN
+ SELECT jsonb_agg(t)
+ INTO tickets
+ FROM (
+ SELECT
+ t.id,
+ t.ticket_symbol,
+ t.occasion,
+ t.note,
+ (
+ SELECT jsonb_agg(opt)
+ FROM (
+ SELECT
+ opt.id,
+ opt.product
+ FROM eshop.order_product_ticket opt
+ WHERE opt.ticket = t.id
+ ) opt
+ ) AS order_product_ticket
+ FROM eshop.tickets t
+ WHERE t.id = ANY(ticket_ids)
+ ) t;
+
+ RETURN tickets;
+END;
+$$;
\ No newline at end of file
diff --git a/supabase/functions/_shared/generateTicket.ts b/supabase/functions/_shared/generateTicket.ts
new file mode 100644
index 000000000..7c69565a4
--- /dev/null
+++ b/supabase/functions/_shared/generateTicket.ts
@@ -0,0 +1,199 @@
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.46.2';
+import { qrcode } from 'https://deno.land/x/qrcode/mod.ts';
+import { createCanvas, loadImage } from "https://deno.land/x/canvas/mod.ts";
+
+// Initialize Supabase client
+const supabaseAdmin = createClient(
+ Deno.env.get('SUPABASE_URL')!,
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+);
+
+/**
+ * Generates a ticket image based on the provided ticket data.
+ * @param ticket - The ticket object containing necessary details.
+ * @returns A Promise that resolves to a Uint8Array containing the image data.
+ */
+export async function generateTicketImage(ticket: any): Promise {
+ try {
+ // 1. Fetch occasion details to get the background image and other data
+ const { data: occasion, error: occasionError } = await supabaseAdmin
+ .from('occasions')
+ .select(`
+ id,
+ data
+ `)
+ .eq('id', ticket.occasion)
+ .single();
+
+ if (occasionError || !occasion) {
+ throw new Error(`Occasion not found or error fetching occasion: ${occasionError?.message}`);
+ }
+
+ // 2. Extract background URL and color from occasion.data.features array
+ let backgroundUrl: string | undefined;
+ let fontColor: string = '#000000';
+
+ if (occasion.data && Array.isArray(occasion.data.features)) {
+ const ticketFeature = occasion.data.features.find(
+ (feature: any) => feature.code === 'ticket'
+ );
+ if (ticketFeature) {
+ if (typeof ticketFeature.background === 'string') {
+ backgroundUrl = ticketFeature.background;
+ }
+
+ if (typeof ticketFeature.color === 'string') {
+ // Validate hex color code
+ const hexColorRegex = /^[A-Fa-f0-9]{6}$/;
+ if (hexColorRegex.test(ticketFeature.color)) {
+ fontColor = `#${ticketFeature.color}`;
+ } else {
+ console.log(`Invalid color format: ${ticketFeature.color}`);
+ }
+ }
+ }
+ }
+
+ if (!backgroundUrl) {
+ throw new Error('Background image URL not found in occasion.data.features.');
+ }
+
+ // 3. Fetch all product types to map product_type IDs to their string types
+ const { data: productTypes, error: productTypesError } = await supabaseAdmin.schema('eshop')
+ .from('product_types')
+ .select('id, type')
+ .throwOnError();
+
+ if (productTypesError || !productTypes) {
+ throw new Error(`Error fetching product types: ${productTypesError?.message}`);
+ }
+
+ // Create a map of product_type ID to type string
+ const productTypeMap: Record = {};
+ productTypes.forEach((pt: any) => {
+ productTypeMap[pt.id] = pt.type;
+ });
+
+ // 4. Fetch related products (spot, food, taxi)
+ const productIds = ticket.order_product_ticket.map((opt: any) => opt.product);
+ const { data: products, error: productsError } = await supabaseAdmin.schema('eshop')
+ .from('products')
+ .select(`
+ id,
+ title,
+ title_short,
+ product_type,
+ data
+ `)
+ .in('id', productIds)
+ .throwOnError();
+
+ if (productsError || !products) {
+ throw new Error(`Error fetching products: ${productsError?.message}`);
+ }
+
+ // Map products by their type string
+ const productMap: Record = {};
+ products.forEach((product: any) => {
+ const type = productTypeMap[product.product_type];
+ if (type) {
+ productMap[type] = product;
+ }
+ });
+
+ const spotProduct = productMap['spot'];
+ const foodProduct = productMap['food'];
+ const taxiProduct = productMap['taxi'];
+
+ // 5. Load background image
+ const backgroundImage = await loadImage(backgroundUrl);
+
+ // 6. Create canvas with background dimensions
+ const canvas = createCanvas(backgroundImage.width(), backgroundImage.height());
+ const ctx = canvas.getContext('2d');
+
+ // 7. Draw background
+ ctx.drawImage(backgroundImage, 0, 0, backgroundImage.width(), backgroundImage.height());
+
+ // 8. Define positions and styles for QR code and text
+ const padding = 175;
+ const qrSize = 200; // Increased size for better visibility
+ const lineHeight = 40;
+
+ // Calculate center positions
+ const centerX = backgroundImage.width() / 2;
+ const lowerHalfStartY = backgroundImage.height() / 2;
+
+ // Set font color and alignment
+ ctx.fillStyle = fontColor;
+ ctx.textAlign = 'center';
+
+ // 9. Generate QR Code based on ticket.ticket_symbol
+ const qrData = ticket.ticket_symbol;
+ const qrBase64 = await qrcode(qrData, { size: qrSize });
+ const qrImage = await loadImage(`data:image/png;base64,${qrBase64.split(',')[1]}`);
+
+ // 10. Create a separate canvas for the grouped object (texts + QR code)
+ const groupCanvasWidth = backgroundImage.width();
+ const groupCanvasHeight = qrSize; // Height based on QR code size
+ const groupCanvas = createCanvas(groupCanvasWidth, groupCanvasHeight);
+ const groupCtx = groupCanvas.getContext('2d');
+
+ // Draw QR code on the group canvas (right side)
+ const groupQrX = groupCanvasWidth - qrSize - padding;
+ const groupQrY = 0;
+ groupCtx.drawImage(qrImage, groupQrX, groupQrY, qrSize, qrSize);
+
+ // Prepare text content
+ const texts: string[] = [];
+
+ // Add Spot Title
+ const spotOrder = ticket.order_product_ticket.find((opt: any) => opt.product === spotProduct.id);
+ if (spotOrder && spotOrder.spot_group_title) {
+ texts.push(`Stůl: ${spotOrder.spot_group_title}`);
+ } else {
+ texts.push(`Stůl: N/A`);
+ }
+
+ // Add Food Title
+ if (foodProduct) {
+ const foodTitle = foodProduct.title_short || foodProduct.title || 'N/A';
+ texts.push(`Večeře: ${foodTitle}`);
+ }
+
+ // Add Taxi Title
+ if (taxiProduct) {
+ const taxiTitle = taxiProduct.title_short || taxiProduct.title || 'N/A';
+ texts.push(`Odvoz: ${taxiTitle}`);
+ }
+
+ // Set common font for all texts
+ ctx.font = 'bold 36px Arial'; // Larger font size
+
+ // Add texts to the group canvas (left side)
+ const textStartX = padding;
+ let currentTextY = (groupCanvasHeight - (texts.length * lineHeight)) / 2 + lineHeight;
+
+ texts.forEach((text) => {
+ groupCtx.font = 'bold 36px Arial';
+ groupCtx.fillStyle = fontColor;
+ groupCtx.fillText(text, textStartX, currentTextY);
+ currentTextY += lineHeight;
+ });
+
+ // Draw the group canvas onto the main canvas, centered in the lower half
+ const groupX = 0; // Since groupCanvasWidth == backgroundImage.width()
+ const groupY = lowerHalfStartY + padding; // Positioned in the lower half with padding
+
+ ctx.drawImage(groupCanvas, groupX, groupY, groupCanvasWidth, groupCanvasHeight);
+
+
+ // 12. Convert the canvas to a buffer
+ const buffer = canvas.toBuffer('image/png');
+ return buffer;
+
+ } catch (error) {
+ console.error('Error generating ticket image:', error);
+ throw error; // Re-throw the error after logging
+ }
+}
diff --git a/supabase/functions/send-ticket-order/index.ts b/supabase/functions/send-ticket-order/index.ts
index 1e652bc74..a2c1701c1 100644
--- a/supabase/functions/send-ticket-order/index.ts
+++ b/supabase/functions/send-ticket-order/index.ts
@@ -211,8 +211,8 @@ Deno.serve(async (req) => {
.insert({
"from": _DEFAULT_EMAIL,
"to": orderDetails.email,
- "template": "TICKET_ORDER_CONFIRMATION",
- "organization": occasion.id,
+ "template": template.data.id,
+ "organization": organizationId,
"occasion": occasion.id,
});
diff --git a/supabase/functions/send-tickets/index.ts b/supabase/functions/send-tickets/index.ts
new file mode 100644
index 000000000..dd3fbf222
--- /dev/null
+++ b/supabase/functions/send-tickets/index.ts
@@ -0,0 +1,207 @@
+
+import { sendEmailWithSubs } from "../_shared/emailClient.ts";
+import { generateTicketImage } from "../_shared/generateTicket.ts"; // Ensure this path is correct
+import { createClient, SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2.46.2';
+import { decode } from "https://deno.land/std@0.140.0/encoding/base64.ts";
+
+// Initialize Supabase client
+const supabaseAdmin: SupabaseClient = createClient(
+ Deno.env.get('SUPABASE_URL')!,
+ Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
+);
+
+// Default sender email
+const _DEFAULT_EMAIL = Deno.env.get("DEFAULT_EMAIL")!;
+
+// CORS Headers
+const corsHeaders = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+};
+
+/**
+ * Checks if the user is an editor for the given occasion.
+ * @param userId - The UUID of the user.
+ * @param occasionId - The ID of the occasion.
+ * @returns A Promise that resolves to a boolean indicating editor status.
+ */
+async function isUserEditor(userId: string, occasionId: bigint): Promise {
+ const { data, error } = await supabaseAdmin
+ .from('occasion_users')
+ .select('is_editor')
+ .eq('user', userId)
+ .eq('occasion', occasionId)
+ .single();
+
+ if (error || !data) {
+ console.error("Error fetching user role:", error);
+ return false;
+ }
+
+ return data.is_editor;
+}
+
+/**
+ * Serves the endpoint to send tickets via email.
+ */
+Deno.serve(async (req) => {
+ try {
+ // Handle CORS preflight request
+ if (req.method === 'OPTIONS') {
+ return new Response('ok', { headers: corsHeaders });
+ }
+
+ const supabaseUser = createClient(
+ Deno.env.get('SUPABASE_URL') ?? '',
+ Deno.env.get('SUPABASE_ANON_KEY') ?? '',
+ { global: { headers: { Authorization: req.headers.get('Authorization')! } } }
+ );
+
+ const { data: user, error: userError } = await supabaseUser.auth.getUser();
+
+ if (userError || !user) {
+ console.error("User authentication failed:", userError);
+ return new Response(JSON.stringify({ error: "Unauthorized" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 401,
+ });
+ }
+
+ const userId = user.user.id;
+ console.log(userId);
+ // Parse request body
+ const reqData = await req.json();
+ const { ticketIds, email, oc } = reqData;
+
+ // Validate input
+ if (!Array.isArray(ticketIds) || typeof email !== 'string' || !oc) {
+ return new Response(JSON.stringify({ error: "Invalid input parameters" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 400,
+ });
+ }
+
+ const occasionId = oc;
+
+ // Check if user is editor for the occasion
+ const userIsEditor = await isUserEditor(userId, occasionId);
+ if (!userIsEditor) {
+ console.error(`User ${userId} is not an editor for occasion ${occasionId}`);
+ return new Response(JSON.stringify({ error: "Forbidden: User is not an editor for this occasion" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 403,
+ });
+ }
+
+ // Fetch occasion details for email template
+ const { data: occasionData, error: occasionError } = await supabaseAdmin
+ .from("occasions")
+ .select("organization, title")
+ .eq("id", occasionId)
+ .single();
+
+ if (occasionError || !occasionData) {
+ console.error("Occasion not found:", occasionError);
+ return new Response(JSON.stringify({ error: "Occasion not found" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 404,
+ });
+ }
+
+ const organizationId = occasionData.organization;
+ const occasionTitle = occasionData.title;
+
+ // Fetch email template
+ const { data: template, error: templateError } = await supabaseAdmin
+ .from("email_templates")
+ .select()
+ .eq("organization", organizationId)
+ .eq("occasion", occasionId)
+ .eq("code", "TICKET_ORDER_PAYMENT_DONE")
+ .single();
+
+ if (templateError || !template) {
+ console.error("Email template not found for the occasion:", templateError);
+ return new Response(JSON.stringify({ error: "Email template not found" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 404,
+ });
+ }
+
+ console.log(ticketIds);
+ const { data: tickets } = await supabaseAdmin.rpc("get_tickets_with_details", {
+ ticket_ids: ticketIds
+ });
+
+ if (!tickets) {
+ console.error("Error fetching tickets:", ticketsError);
+ return new Response(JSON.stringify({ error: "Error fetching tickets" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 500,
+ });
+ }
+
+ // Generate ticket images
+ const attachments = [];
+ for (const ticket of tickets) {
+ try {
+ console.log("generating...");
+ const ticketImage = await generateTicketImage(ticket);
+
+ attachments.push({
+ filename: `ticket_${ticket.ticket_symbol}.png`,
+ content: ticketImage, // Uint8Array
+ contentType: "image/png",
+ encoding: "binary",
+ });
+ } catch (imageError) {
+ console.error(`Error generating image for ticket ${ticket.id}:`, imageError);
+ // Continue with other tickets or handle accordingly
+ }
+ }
+
+ if (attachments.length === 0) {
+ return new Response(JSON.stringify({ error: "No valid ticket images generated" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 400,
+ });
+ }
+
+ // Prepare email substitutions
+ const subs = {
+ occasionTitle: occasionTitle,
+ };
+
+ // Send email
+ await sendEmailWithSubs({
+ to: email,
+ subject: template.subject,
+ content: template.html,
+ subs,
+ from: `${occasionTitle} | Festapp <${_DEFAULT_EMAIL}>`,
+ attachments: attachments,
+ });
+
+ await supabaseAdmin
+ .from("log_emails")
+ .insert({
+ "from": _DEFAULT_EMAIL,
+ "to": email,
+ "template": template.id,
+ "organization": organizationId,
+ "occasion": occasionId,
+ });
+
+ return new Response(JSON.stringify({ message: "Tickets sent successfully", code: 200 }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 200,
+ });
+
+ } catch (error) {
+ console.error("Unexpected error:", error);
+ return new Response(JSON.stringify({ error: "Unexpected error occurred" }), {
+ headers: { ...corsHeaders, 'Content-Type': 'application/json' },
+ status: 500,
+ });
+ }
+});