From 769644fb274bb90ff6b8f43e3f8149fcfa001c99 Mon Sep 17 00:00:00 2001 From: otrok7 <50595291+otrok7@users.noreply.github.com> Date: Sun, 19 Oct 2025 15:35:50 +0200 Subject: [PATCH 01/13] Purified from other branch --- .../Controllers/Admin/MeetingController.php | 20 +- .../Admin/Swagger/MeetingController.php | 5 +- .../Controllers/Query/SwitcherController.php | 7 + .../Http/Resources/Admin/MeetingResource.php | 20 ++ .../Http/Resources/Query/MeetingResource.php | 22 ++- .../Interfaces/MeetingRepositoryInterface.php | 1 + src/app/Models/Meeting.php | 31 ++- src/app/Repositories/MeetingRepository.php | 110 +++++++++-- ...025_10_19_131156_add_group_to_meetings.php | 30 +++ .../js/components/MeetingEditForm.svelte | 179 ++++++++++++++++-- src/resources/js/lang/de.ts | 1 + src/resources/js/lang/en.ts | 1 + src/storage/api-docs/api-docs.json | 12 ++ 13 files changed, 400 insertions(+), 39 deletions(-) create mode 100644 src/database/migrations/2025_10_19_131156_add_group_to_meetings.php diff --git a/src/app/Http/Controllers/Admin/MeetingController.php b/src/app/Http/Controllers/Admin/MeetingController.php index abbfaf273..b306b565e 100644 --- a/src/app/Http/Controllers/Admin/MeetingController.php +++ b/src/app/Http/Controllers/Admin/MeetingController.php @@ -55,6 +55,7 @@ public function index(Request $request) weekdaysInclude: $days, servicesInclude: $serviceBodyIds, searchString: $searchString, + returnGroups: true, ); return MeetingResource::collection($meetings); @@ -189,6 +190,13 @@ private function validateInputs(Request $request, $skipVenueTypeLocationValidati 'worldId' => 'nullable|string|max:30', 'name' => 'required|string|max:128', 'timeZone' => ['nullable', 'string', 'max:40', new IANATimeZone], + 'membersOfGroup' => 'sometimes|nullable|array', + 'membersOfGroup.*.id_bigint' => 'nullable|int|exists:comdef_meetings_main,id_bigint', + 'membersOfGroup.*.day' => 'required|int|between:0,6', + 'membersOfGroup.*.startTime' => 'required|date_format:H:i', + 'membersOfGroup.*.duration' => 'required|date_format:H:i', + 'membersOfGroup.*.formatIds' => 'present|array', + 'membersOfGroup.*.formatIds.*' => ['int', 'exists:comdef_formats,shared_id_bigint', Rule::notIn([$this->getVirtualFormatId(), $this->getTemporarilyClosedFormatId(), $this->getHybridFormatId()])], ], $this->getDataFieldValidators($skipVenueTypeLocationValidation)) )); } @@ -272,7 +280,17 @@ private function buildValuesArray(Collection $validated): array 'worldid_mixed' => $validated['worldId'] ?? null, 'meeting_name' => $validated['name'], ]; - + $values['membersOfGroup'] = []; + foreach ($validated['membersOfGroup'] ?? [] as $member) { + $member['venueType'] = $validated['venueType']; + $values['membersOfGroup'][] = [ + 'id_bigint' => $member['id_bigint'] ?? null, + 'weekday_tinyint' => $member['day'], + 'start_time' => \DateTime::createFromFormat('H:i', $member['startTime'])->format('H:i:s'), + 'duration_time' => \DateTime::createFromFormat('H:i', $member['duration'])->format('H:i:s'), + 'formats' => $this->buildFormatsString(collect($member)), + ]; + } $customFields = $this->getCustomFields(); return collect($values) diff --git a/src/app/Http/Controllers/Admin/Swagger/MeetingController.php b/src/app/Http/Controllers/Admin/Swagger/MeetingController.php index 73b7ecd09..903bbd843 100644 --- a/src/app/Http/Controllers/Admin/Swagger/MeetingController.php +++ b/src/app/Http/Controllers/Admin/Swagger/MeetingController.php @@ -43,8 +43,11 @@ * @OA\Property(property="bus_lines", type="string", example="string"), * @OA\Property(property="train_lines", type="string", example="string"), * @OA\Property(property="comments", type="string", example="string"), + * @OA\Property(property="membersOfGroup",type="array", + * @OA\Items(type="object", example={"day": "0", "startTime":"19:00", "duration": "01:30", "formats": "[]"}), + * ), * @OA\Property(property="customFields", type="object", example={"key1": "value1", "key2": "value2"}, - * @OA\AdditionalProperties(type="string") + * @OA\AdditionalProperties(type="string"), * ), * ), * @OA\Schema(schema="Meeting", required={"id", "serviceBodyId", "formatIds", "venueType", "temporarilyVirtual", "day", "startTime", "duration", "timeZone", "latitude", "longitude", "published", "email", "worldId", "name"}, diff --git a/src/app/Http/Controllers/Query/SwitcherController.php b/src/app/Http/Controllers/Query/SwitcherController.php index ca5e89a8f..bb3309f81 100644 --- a/src/app/Http/Controllers/Query/SwitcherController.php +++ b/src/app/Http/Controllers/Query/SwitcherController.php @@ -256,6 +256,12 @@ private function getSearchResults(Request $request, ?string $dataFormat = null): } else { $published = false; } + $returnGroups = $request->input('return_groups', '0'); + if ($returnGroups == '1') { + $returnGroups = true; + } else { + $returnGroups = false; + } $sortKeys = $request->input('sort_keys'); $sortKeys = empty($sortKeys) ? null : explode(',', $sortKeys); @@ -353,6 +359,7 @@ private function getSearchResults(Request $request, ?string $dataFormat = null): sortKeys: $sortKeys, pageSize: $pageSize, pageNum: $pageNum, + returnGroups: $returnGroups, ); // This code to calculate the formats fields is really inefficient, but necessary because diff --git a/src/app/Http/Resources/Admin/MeetingResource.php b/src/app/Http/Resources/Admin/MeetingResource.php index 070289e3c..2ff764f54 100644 --- a/src/app/Http/Resources/Admin/MeetingResource.php +++ b/src/app/Http/Resources/Admin/MeetingResource.php @@ -62,6 +62,24 @@ public function toArray($request) ->reject(fn ($id) => !self::$formatsById->has($id)) ->sort(); + $membersOfGroup = []; + foreach ($this->groupMembers->toResourceCollection(MeetingResource::class) as $member) { + $memberFormatIds = empty($member->formats) ? collect([]) : collect(explode(',', $member->formats)) + ->map(fn ($id) => intval($id)) + ->reject(fn ($id) => !self::$formatsById->has($id)) + ->sort(); + $membersOfGroup[] = [ + 'id_bigint' => $member->id_bigint, + 'day' => $member->weekday_tinyint, + 'startTime' => is_null($member->start_time) ? null : (\DateTime::createFromFormat('H:i:s', $member->start_time) ?: \DateTime::createFromFormat('H:i', $member->start_time))->format('H:i'), + 'duration' => is_null($member->duration_time) ? null : (\DateTime::createFromFormat('H:i:s', $member->duration_time) ?: \DateTime::createFromFormat('H:i', $member->duration_time))->format('H:i'), + 'formats' => $memberFormatIds, + ]; + usort($membersOfGroup, fn($a, $b) => $a['day'] <=> $b['day'] ?: $a['startTime'] <=> $b['startTime']); + }; + if (!empty($membersOfGroup)) { + $membersOfGroup = ['membersOfGroup' => $membersOfGroup]; + } return array_merge( [ 'id' => $this->id_bigint, @@ -79,7 +97,9 @@ public function toArray($request) 'email' => $this->email_contact ?: null, 'worldId' => $this->worldid_mixed ?: null, 'name' => $meetingData->get('meeting_name') ?: null, + 'is_group' => $this->is_group ?? 0, ], + $membersOfGroup, self::$dataTemplates ->reject(fn ($t, $_) => self::$customFields->contains($t->key)) ->mapWithKeys(fn ($t, $_) => [$t->key => $meetingData->get($t->key) ?: null]) diff --git a/src/app/Http/Resources/Query/MeetingResource.php b/src/app/Http/Resources/Query/MeetingResource.php index b7b3e307e..64ca50ae6 100644 --- a/src/app/Http/Resources/Query/MeetingResource.php +++ b/src/app/Http/Resources/Query/MeetingResource.php @@ -96,7 +96,20 @@ public function toArray($request) $meeting[$meetingDataTemplate->key] = $meetingData->get($meetingDataTemplate->key, '') ?? ''; } - + // Could be expensive. Maybe we want to control when this is done.... + if ($this->getIsGroup() && (!self::$hasDataFieldKeys || self::$dataFieldKeys->has('membersOfGroup'))) { + $meeting['membersOfGroup'] = []; + foreach ($this->groupMembers->toResourceCollection(MeetingResource::class) as $member) { + $meeting['membersOfGroup'][] = [ + 'id_bigint' => $member->getIdBigint(), + 'weekday_tinyint' => $member->getWeekdayTinyint(), + 'start_time' => $member->getStartTime(), + 'duration_time' => $member->getDurationTime(), + 'formats' => $member->getFormats(), + ]; + } + usort($meeting['membersOfGroup'], fn($a, $b) => $a['weekday_tinyint'] <=> $b['weekday_tinyint'] ?: $a['start_time'] <=> $b['start_time']); + } return $meeting; } @@ -248,6 +261,13 @@ private function getLangEnum() $this->lang_enum ?? '' ); } + private function getIsGroup() + { + return $this->when( + !self::$hasDataFieldKeys || self::$dataFieldKeys->has('is_group'), + $this->is_group ?? 0 + ); + } private function getLongitude() { return $this->when( diff --git a/src/app/Interfaces/MeetingRepositoryInterface.php b/src/app/Interfaces/MeetingRepositoryInterface.php index ea7992f04..805c3e24e 100644 --- a/src/app/Interfaces/MeetingRepositoryInterface.php +++ b/src/app/Interfaces/MeetingRepositoryInterface.php @@ -41,6 +41,7 @@ public function getSearchResults( array $sortKeys = null, int $pageSize = null, int $pageNum = null, + bool $returnGroups = false, ): Collection; public function getFieldKeys(): Collection; public function getFieldValues(string $fieldName, array $specificFormats = [], bool $allFormats = false): Collection; diff --git a/src/app/Models/Meeting.php b/src/app/Models/Meeting.php index bc8e06324..531ae0d1a 100644 --- a/src/app/Models/Meeting.php +++ b/src/app/Models/Meeting.php @@ -26,6 +26,8 @@ class Meeting extends Model 'latitude', 'published', 'email_contact', + 'is_group', + 'group_id' ]; public static $mainFields = [ @@ -81,7 +83,22 @@ public function longData() { return $this->hasMany(MeetingLongData::class, 'meetingid_bigint'); } - + public function group() + { + return $this->belongsTo(Meeting::class, 'group_id', 'id_bigint'); + } + public function groupMembers() + { + return $this->hasMany(Meeting::class, 'group_id'); + } + public function groupData() + { + return $this->hasMany(MeetingData::class, 'meetingid_bigint', 'group_id'); + } + public function groupLongData() + { + return $this->hasMany(MeetingLongData::class, 'meetingid_bigint', 'group_id'); + } private ?string $calculatedFormatKeys = null; private function setCalculatedFormatKeys(string $formatKeyStrings) { @@ -106,12 +123,16 @@ public function getCalculatedFormatSharedIds(): string public function calculateFormatsFields(Collection $formatsById) { - if (is_null($this->formats) || $this->formats == '') { + $formatIds = []; + if (!is_null($this->formats) && !$this->formats == '') { + $formatIds = explode(',', $this->formats); + } + if (!is_null($this->group) && !is_null($this->group->formats) && !$this->group->formats == '') { + $formatIds = array_merge($formatIds, explode(',', $this->group->formats)); + } + if (count($formatIds) === 0) { return; } - - $formatIds = explode(',', $this->formats); - $calculatedFormats = []; foreach ($formatIds as $formatId) { $format = $formatsById->get(intval($formatId)); diff --git a/src/app/Repositories/MeetingRepository.php b/src/app/Repositories/MeetingRepository.php index ebd58c4f7..0bc1dea4a 100644 --- a/src/app/Repositories/MeetingRepository.php +++ b/src/app/Repositories/MeetingRepository.php @@ -51,21 +51,31 @@ public function getSearchResults( array $sortKeys = null, int $pageSize = null, int $pageNum = null, + bool $returnGroups = false ): Collection { - $eagerLoadRelations = ['data', 'longdata']; + $eagerLoadRelations = ['data', 'longdata', 'group']; if ($eagerServiceBodies) { $eagerLoadRelations[] = 'serviceBody'; } if ($eagerRootServers) { $eagerLoadRelations[] = 'rootServer'; } + if ($returnGroups) { + $eagerLoadRelations[] = 'groupMembers'; + } $meetings = Meeting::with($eagerLoadRelations); if (!is_null($published)) { $meetings = $meetings->where('published', $published ? 1 : 0); } - + if (!is_null($returnGroups)) { + if ($returnGroups) { + $meetings = $meetings->whereNull('group_id'); + } else { + $meetings = $meetings->where('is_group', 0); + } + } if (!is_null($meetingIdsInclude)) { $meetings = $meetings->whereIn('id_bigint', $meetingIdsInclude); } @@ -114,7 +124,13 @@ public function getSearchResults( ->orWhere('formats', "$formatId") ->orWhere('formats', 'LIKE', "$formatId,%") ->orWhere('formats', 'LIKE', "%,$formatId,%") - ->orWhere('formats', 'LIKE', "%,$formatId"); + ->orWhere('formats', 'LIKE', "%,$formatId") + ->orWhereHas('group', function (Builder $query) use ($formatId) { + $query->where('formats', "$formatId") + ->orWhere('formats', 'LIKE', "$formatId,%") + ->orWhere('formats', 'LIKE', "%,$formatId,%") + ->orWhere('formats', 'LIKE', "%,$formatId"); + }); }); } } else { @@ -125,7 +141,13 @@ public function getSearchResults( ->orWhere('formats', "$formatId") ->orWhere('formats', 'LIKE', "$formatId,%") ->orWhere('formats', 'LIKE', "%,$formatId,%") - ->orWhere('formats', 'LIKE', "%,$formatId"); + ->orWhere('formats', 'LIKE', "%,$formatId") + ->orWhereHas('group', function (Builder $query) use ($formatId) { + $query->where('formats', "$formatId") + ->orWhere('formats', 'LIKE', "$formatId,%") + ->orWhere('formats', 'LIKE', "%,$formatId,%") + ->orWhere('formats', 'LIKE', "%,$formatId"); + }); }); } }); @@ -155,6 +177,9 @@ public function getSearchResults( ->whereHas('data', function (Builder $query) use ($meetingKey, $meetingKeyValue) { $query->where('key', $meetingKey)->where('data_string', $meetingKeyValue); }) + ->orWhereHas('groupData', function (Builder $query) use ($meetingKey, $meetingKeyValue) { + $query->where('key', $meetingKey)->where('data_string', $meetingKeyValue); + }) ->orWhereHas('longdata', function (Builder $query) use ($meetingKey, $meetingKeyValue) { $query->where('key', $meetingKey)->where('data_blob', $meetingKeyValue); }); @@ -624,9 +649,39 @@ public function create(array $values): Meeting $mainValues = $values->reject(fn ($_, $fieldName) => !in_array($fieldName, Meeting::$mainFields))->toArray(); $dataTemplates = $this->getDataTemplates(); $dataValues = $values->reject(fn ($_, $fieldName) => !$dataTemplates->has($fieldName)); - - return DB::transaction(function () use ($mainValues, $dataValues, $dataTemplates) { + $members = $values['membersOfGroup'] ?? []; + $mainValues['is_group'] = (isset($values['membersOfGroup']) && ($values['membersOfGroup'] !== null)) ? 1 : 0; + $mainValues['group_id'] = null; + if ($mainValues['is_group'] == 1) { + $day = 7; + $time = '24:00'; + foreach ($values['membersOfGroup'] as $member) { + if ($member['weekday_tinyint'] < $day) { + $day = $member['weekday_tinyint']; + $time = $member['start_time']; + } elseif ($member['weekday_tinyint'] == $day && $member['start_time'] < $time) { + $time = $member['start_time']; + } + } + if ($day <= 6) { + $mainValues['weekday_tinyint'] = $day; + $mainValues['start_time'] = $time; + } + } + return DB::transaction(function () use ($mainValues, $dataValues, $dataTemplates, $members) { $meeting = Meeting::create($mainValues); + if ($mainValues['is_group'] == 1) { + $meeting['membersOfGroup'] = []; + foreach ($members as $member) { + $mainValues['start_time'] = $member['start_time']; + $mainValues['weekday_tinyint'] = $member['weekday_tinyint']; + $mainValues['duration_time'] = $member['duration_time']; + $mainValues['formats'] = $member['formats']; + $mainValues['is_group'] = 0; + $mainValues['group_id'] = $meeting->id_bigint; + Meeting::create($mainValues); + } + } foreach ($dataValues as $fieldName => $fieldValue) { $t = $dataTemplates->get($fieldName); if (strlen($fieldValue) > 255) { @@ -660,15 +715,47 @@ public function update(int $id, array $values): bool { $values = collect($values); $mainValues = $values->reject(fn ($_, $fieldName) => !in_array($fieldName, Meeting::$mainFields))->toArray(); + $mainValues['is_group'] = (isset($values['membersOfGroup']) && ($values['membersOfGroup'] !== null)) ? 1 : 0; + $mainValues['group_id'] = null; + if ($mainValues['is_group'] == 1) { + $day = 7; + $time = '24:00'; + foreach ($values['membersOfGroup'] as $member) { + if ($member['weekday_tinyint'] < $day) { + $day = $member['weekday_tinyint']; + $time = $member['start_time']; + } elseif ($member['weekday_tinyint'] == $day && $member['start_time'] < $time) { + $time = $member['start_time']; + } + } + if ($day <= 6) { + $mainValues['weekday_tinyint'] = $day; + $mainValues['start_time'] = $time; + } + } $dataTemplates = $this->getDataTemplates(); $dataValues = $values->reject(fn ($_, $fieldName) => !$dataTemplates->has($fieldName)); - - return DB::transaction(function () use ($id, $mainValues, $dataValues, $dataTemplates) { + $members = $values['membersOfGroup'] ?? []; + return DB::transaction(function () use ($id, $mainValues, $dataValues, $dataTemplates, $members) { $meeting = Meeting::find($id); $meeting->loadMissing(['data', 'longdata']); if (!is_null($meeting)) { Meeting::query()->where('id_bigint', $id)->update($mainValues); - MeetingData::query()->where('meetingid_bigint', $id)->delete(); + Meeting::query()->where('group_id', $id)->whereNotIn('id_bigint', array_column($members, 'id_bigint'))->delete(); + foreach ($members as $member) { + $mainValues['start_time'] = $member['start_time']; + $mainValues['weekday_tinyint'] = $member['weekday_tinyint']; + $mainValues['duration_time'] = $member['duration_time']; + $mainValues['formats'] = $member['formats']; + $mainValues['is_group'] = 0; + $mainValues['group_id'] = $id; + if (!empty($member['id_bigint'])) { + Meeting::query()->where('id_bigint', $member['id_bigint'])->update($mainValues); + } else { + Meeting::create($mainValues); + } + } + MeetingData::query()->where('meetingid_bigint', $id)->where('lang_enum', $this->getRequestedLanguage())->delete(); MeetingLongData::query()->where('meetingid_bigint', $id)->delete(); foreach ($dataValues as $fieldName => $fieldValue) { $t = $dataTemplates->get($fieldName); @@ -709,6 +796,7 @@ public function delete(int $id): bool $meeting->loadMissing(['data', 'longdata']); MeetingData::query()->where('meetingid_bigint', $meeting->id_bigint)->delete(); MeetingLongData::query()->where('meetingid_bigint', $meeting->id_bigint)->delete(); + Meeting::query()->where('group_id', $meeting->id_bigint)->delete(); Meeting::query()->where('id_bigint', $meeting->id_bigint)->delete(); if (!legacy_config('aggregator_mode_enabled')) { $this->saveChange($meeting, null); @@ -858,10 +946,10 @@ public function import(int $rootServerId, Collection $externalObjects): void if (is_null($db)) { $values = $this->externalMeetingToValuesArray($rootServerId, $serviceBodyId, $external, $formatSourceIdToSharedIdMap); - $this->create($values); + $this->create($values, 'en'); } else if (!$external->isEqual($db, $serviceBodyIdToSourceIdMap, $formatSharedIdToSourceIdMap)) { $values = $this->externalMeetingToValuesArray($rootServerId, $serviceBodyId, $external, $formatSourceIdToSharedIdMap); - $this->update($db->id_bigint, $values); + $this->update($db->id_bigint, $values, 'en'); } } } diff --git a/src/database/migrations/2025_10_19_131156_add_group_to_meetings.php b/src/database/migrations/2025_10_19_131156_add_group_to_meetings.php new file mode 100644 index 000000000..cc4f210b8 --- /dev/null +++ b/src/database/migrations/2025_10_19_131156_add_group_to_meetings.php @@ -0,0 +1,30 @@ +boolean('is_group')->default(0); + $table->integer('group_id')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('comdef_meetings_main', function (Blueprint $table) { + $table->dropColumn('is_group'); + $table->dropColumn('group_id'); + }); + } +}; diff --git a/src/resources/js/components/MeetingEditForm.svelte b/src/resources/js/components/MeetingEditForm.svelte index 046eaa0e1..3610cba78 100644 --- a/src/resources/js/components/MeetingEditForm.svelte +++ b/src/resources/js/components/MeetingEditForm.svelte @@ -26,7 +26,7 @@ import type { Format, Meeting, MeetingPartialUpdate, ServiceBody } from 'bmlt-server-client'; import { translations } from '../stores/localization'; import MeetingDeleteModal from './MeetingDeleteModal.svelte'; - import { TrashBinOutline } from 'flowbite-svelte-icons'; + import { TrashBinOutline, PlusOutline } from 'flowbite-svelte-icons'; interface Props { selectedMeeting: Meeting | null; @@ -40,10 +40,16 @@ let { selectedMeeting, serviceBodies, formats, onSaved, onDeleted }: Props = $props(); const daysOfWeek: string[] = [$translations.day0, $translations.day1, $translations.day2, $translations.day3, $translations.day4, $translations.day5, $translations.day6]; - - const tabs = selectedMeeting - ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] - : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther]; + let tabs = $state(selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsMeetingTimes, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther]); + let tabsSnippets = $state(selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [basicTabContent, locationTabContent, meetingTimesTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent]); const globalSettings = settings; const seenNames = new SvelteSet(); const ignoredFormats = ['VM', 'HY', 'TC']; @@ -112,6 +118,17 @@ const [hours, minutes] = globalSettings.defaultDuration.split(':').map((part) => part.padStart(2, '0')); defaultDuration = hours + ':' + minutes; } + type GroupMember = { id_bigint?: number, day: number; startTime: string; duration: string; formatIds: [] }; + let groupMembers: GroupMember[] = []; + if (selectedMeeting?.membersOfGroup && selectedMeeting.membersOfGroup.length > 0) { + groupMembers = (selectedMeeting.membersOfGroup as Array<{ id_bigint?: number, day?: number; startTime?: string; duration?: string; formats?: [] }>).map((member) => ({ + id_bigint: member.id_bigint ?? undefined, + day: member.day ?? 0, + startTime: member.startTime ?? '12:00', + duration: member.duration ?? defaultDuration, + formatIds: member.formats ?? [] + })); + } const initialValues = { serviceBodyId: selectedMeeting?.serviceBodyId ?? -1, formatIds: selectedMeeting?.formatIds ?? [], @@ -154,12 +171,14 @@ ...Object.fromEntries(globalSettings.customFields.map((field) => [field.name, ''])), ...Object.fromEntries(Object.entries(selectedMeeting.customFields).map(([key, value]) => [key, value ?? ''])) } - : Object.fromEntries(globalSettings.customFields.map((field) => [field.name, ''])) + : Object.fromEntries(globalSettings.customFields.map((field) => [field.name, ''])), + membersOfGroup: groupMembers, }; let latitude = $state(initialValues.latitude); let longitude = $state(initialValues.longitude); let manualDrag = false; let formatIdsSelected = $state(initialValues.formatIds); + let membersOfGroup = $state(initialValues.membersOfGroup); let savedMeeting: Meeting; let changes: MeetingChangeResource[] = $state([]); let changesLoaded = $state(false); @@ -249,6 +268,15 @@ } } + // These values will be ignored by the server, but set them to safe values to avoid validation errors + // I'd much prefer to delete these, as that is what the server expects, but I'd have to make the + // properties optional, and this seems like the more maintainable approach for now. + if (values.membersOfGroup && values.membersOfGroup.length > 0) { + values.day = 0; + values.startTime = '00:00'; + values.duration = '01:00'; + } + if (selectedMeeting && saveAsCopy) { const copyData = { ...values, @@ -319,15 +347,29 @@ formatIds: yup.array().of(yup.number()), venueType: yup.number().oneOf(VALID_VENUE_TYPES).required(), temporarilyVirtual: yup.bool(), - day: yup.number().integer().min(0).max(6).required(), + day: yup.number().default(-1).when('membersOfGroup', { + is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, + then: (schema) => schema.integer().min(0).max(6).required($translations.dayErrorMessage), + otherwise: (schema) => schema.notRequired() + }), startTime: yup .string() - .matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) - .required(), // HH:mm + .default('') + .when('membersOfGroup', { + is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, + then: (schema) => schema.matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm + .required($translations.startTimeErrorMessage), + otherwise: (schema) => schema.notRequired() + }), duration: yup .string() - .matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) - .required(), // HH:mm + .default('') + .matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm + .when('membersOfGroup', { + is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, + then: (schema) => schema.required($translations.startTimeErrorMessage), + otherwise: (schema) => schema.notRequired() + }), timeZone: yup .string() .oneOf([...timeZones, ''], $translations.timeZoneInvalid) @@ -767,7 +809,33 @@ }); $effect(() => { setData('formatIds', formatIdsSelected); + membersOfGroup.forEach((member,i) => {setData('membersOfGroup.'+i+'.formatIds', member.formatIds)}); }); + function handleDeleteMember(i: number, setData: (d: any, v: any)=>void) { + if (membersOfGroup.length <= 1) return; + membersOfGroup.splice(i, 1); + setData('membersOfGroup', membersOfGroup); + } + function handleAdd() { + membersOfGroup.push({ day: 0, startTime: "12:00", duration: "01:30", formatIds:[] }); + setData('membersOfGroup', membersOfGroup); + } + function convertToGroup() { + tabs.splice(2, 0, $translations.tabsMeetingTimes); + tabsSnippets.splice(2, 0, meetingTimesTabContent); + membersOfGroup = [{ day: $data?.day, startTime: $data?.startTime, duration: $data?.duration, formatIds:[] }]; + setData('membersOfGroup', membersOfGroup); + } + function getDuration(i: number): string { + return membersOfGroup[i].duration ?? "01:00"; + } + function setDuration(i: number, d: string, setData: (d: any, v: any)=>void) { + membersOfGroup[i].duration = d; + setData("membersOfGroup."+i+".duration", d); + } + function isGroup(): boolean { + return tabs.includes($translations.tabsMeetingTimes); + } @@ -821,6 +889,15 @@ Meeting ID: {selectedMeeting.id} + {#if !isGroup()} + + {/if} {/if} + {#if !selectedMeeting && !isGroup()} + + {/if}
@@ -847,7 +933,7 @@
- {#each timeZoneGroups as continent} {#each continent.values as timezone} @@ -863,10 +949,11 @@ {/if}
+ {#if !isGroup()}
- {#if $errors.day} {$errors.day} @@ -892,10 +979,11 @@ {/if}
+ {/if}
- {#if $errors.serviceBodyId} {$errors.serviceBodyId} @@ -942,12 +1030,11 @@ {/if}
{/snippet} - {#snippet locationTabContent()}
- {#if $errors.venueType} {$errors.venueType} @@ -1074,7 +1161,7 @@
{#if countiesAndSubProvincesChoices.length > 0} - {:else} {/if} @@ -1089,7 +1176,7 @@
{#if statesAndProvincesChoices.length > 0} - {:else} {/if} @@ -1152,7 +1239,59 @@
{/snippet} - +{#snippet meetingTimesTabContent()} +
+ +
+ {#each membersOfGroup as member, i} + {#if membersOfGroup[i]?.id_bigint} + + {/if} +
+
+
+
+
+ + +
+
+ {$translations.durationTitle} + setDuration(i,d,setData)} /> +
+
+
+
+ +
+
+ + + {#snippet children({ item, clear })} +
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="presentation"> + + {item.name} + +
+ {/snippet} +
+
+
+ {/each} +{/snippet} {#snippet otherTabContent()}
@@ -1286,7 +1425,7 @@ {/snippet}
- +
diff --git a/src/resources/js/lang/de.ts b/src/resources/js/lang/de.ts index 8dd73b553..7d4305cc6 100644 --- a/src/resources/js/lang/de.ts +++ b/src/resources/js/lang/de.ts @@ -209,6 +209,7 @@ export const deTranslations = { tabsBasic: 'Basic', tabsChanges: 'Änderungen', tabsLocation: 'Standort', + tabsMeetingTimes: 'Meeting Zeiten', tabsOther: 'Andere', technicalDetails: 'Technische Details', time: 'Zeit', diff --git a/src/resources/js/lang/en.ts b/src/resources/js/lang/en.ts index 8780ad9a6..6ca0db845 100644 --- a/src/resources/js/lang/en.ts +++ b/src/resources/js/lang/en.ts @@ -263,6 +263,7 @@ export const enTranslations = { tabsBasic: 'Basic', tabsChanges: 'Changes', tabsLocation: 'Location', + tabsMeetingTimes: 'Meeting Times', tabsOther: 'Other', technicalDetails: 'Technical Details', time: 'Time', diff --git a/src/storage/api-docs/api-docs.json b/src/storage/api-docs/api-docs.json index 3a3342cf7..136865ae6 100644 --- a/src/storage/api-docs/api-docs.json +++ b/src/storage/api-docs/api-docs.json @@ -2489,6 +2489,18 @@ "type": "string", "example": "string" }, + "membersOfGroup": { + "type": "array", + "items": { + "type": "object", + "example": { + "day": "0", + "startTime": "19:00", + "duration": "01:30", + "formats": "[]" + } + } + }, "customFields": { "type": "object", "example": { From 5c4fe4fd2a56f914f9b40cd9111ad4406cbfd20d Mon Sep 17 00:00:00 2001 From: jbraswell <10187286+jbraswell@users.noreply.github.com> Date: Sun, 19 Oct 2025 21:59:53 -0400 Subject: [PATCH 02/13] log aggregator server list and make invalid root server fatal (#1314) --- src/app/Console/Commands/ImportRootServers.php | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/app/Console/Commands/ImportRootServers.php b/src/app/Console/Commands/ImportRootServers.php index 33f7e2297..89319c435 100644 --- a/src/app/Console/Commands/ImportRootServers.php +++ b/src/app/Console/Commands/ImportRootServers.php @@ -84,16 +84,8 @@ private function importRootServersList(RootServerRepositoryInterface $rootServer { try { $url = $this->option('list-url'); - $response = $this->httpGet($url); - $externalRootServers = collect($response) - ->map(function ($rootServer) { - try { - return new ExternalRootServer($rootServer); - } catch (InvalidObjectException) { - return null; - } - }) - ->reject(fn($e) => is_null($e)); + $response = $this->httpGet($url, true); + $externalRootServers = collect($response)->map(fn ($rs) => new ExternalRootServer($rs)); $rootServerRepository->import($externalRootServers); } catch (\Exception $e) { $this->error($e->getMessage()); @@ -261,13 +253,17 @@ private function analyzeTables(): void } } - private function httpGet(string $url): array + private function httpGet(string $url, bool $shouldLogResponse = false): array { sleep(self::$requestDelaySeconds); $headers = ['User-Agent' => 'Mozilla/5.0 (X11; Linux x86_64; rv:52.0) Gecko/20100101 Firefox/52.0 +aggregator']; $response = Http::withHeaders($headers)->retry(3, self::$retryDelaySeconds * 1000)->get($url); + if ($shouldLogResponse) { + $this->info("Response from $url: $response"); + } + if (!$response->ok()) { throw new \Exception("Got bad status code {$response->status()} from $url"); } From 1582fd98e3e8bebc1d2b2970380e5a9cc33dad1e Mon Sep 17 00:00:00 2001 From: pjaudiomv <34245618+pjaudiomv@users.noreply.github.com> Date: Sun, 19 Oct 2025 22:00:40 -0400 Subject: [PATCH 03/13] add revision and date to docker builds (#1315) --- .github/workflows/docker-base.yml | 1 + Makefile | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-base.yml b/.github/workflows/docker-base.yml index 9a25b7a61..dbe3a2f78 100644 --- a/.github/workflows/docker-base.yml +++ b/.github/workflows/docker-base.yml @@ -45,6 +45,7 @@ jobs: org.opencontainers.image.vendor=BMLT org.opencontainers.image.created={{date 'YYYY-MM-DDTHH:mm:ssZ'}} org.opencontainers.image.version=${{ matrix.php_version }} + org.opencontainers.image.revision=${{ github.sha }} php.version=${{ matrix.php_version }} - name: Build and push Base diff --git a/Makefile b/Makefile index eb9ee9ef6..e2d105671 100644 --- a/Makefile +++ b/Makefile @@ -119,7 +119,7 @@ zip: $(ZIP_FILE) ## Builds zip file .PHONY: docker docker: zip ## Builds Docker Image - docker build --pull --build-arg PHP_VERSION=$(BASE_IMAGE_TAG) -f docker/$(DOCKERFILE) . -t $(IMAGE):$(TAG) + docker build --pull --build-arg PHP_VERSION=$(BASE_IMAGE_TAG) --label "org.opencontainers.image.revision=$(COMMIT)" --label "org.opencontainers.image.created=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" -f docker/$(DOCKERFILE) . -t $(IMAGE):$(TAG) .PHONY: docker-push docker-push: ## Pushes docker image to Dockerhub @@ -169,7 +169,7 @@ phpstan: ## PHP Larastan Code Analysis .PHONY: docker-publish-base docker-publish-base: ## Builds Base Docker Image - docker buildx build --platform linux/amd64,linux/arm64/v8 -f docker/Dockerfile-base docker/ -t $(BASE_IMAGE):$(BASE_IMAGE_TAG) --push + docker buildx build --platform linux/amd64,linux/arm64/v8 --label "org.opencontainers.image.revision=$(COMMIT)" --label "org.opencontainers.image.created=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')" -f docker/Dockerfile-base docker/ -t $(BASE_IMAGE):$(BASE_IMAGE_TAG) --push .PHONY: mysql mysql: ## Runs mysql cli in mysql container From 7fa1eafc24b37cebe78886d5fd686b0bc0e873cd Mon Sep 17 00:00:00 2001 From: otrok7 <50595291+otrok7@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:01:28 +0200 Subject: [PATCH 04/13] de-lint --- .../js/components/MeetingEditForm.svelte | 191 +++++++++--------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/src/resources/js/components/MeetingEditForm.svelte b/src/resources/js/components/MeetingEditForm.svelte index 3610cba78..267d24f07 100644 --- a/src/resources/js/components/MeetingEditForm.svelte +++ b/src/resources/js/components/MeetingEditForm.svelte @@ -40,16 +40,20 @@ let { selectedMeeting, serviceBodies, formats, onSaved, onDeleted }: Props = $props(); const daysOfWeek: string[] = [$translations.day0, $translations.day1, $translations.day2, $translations.day3, $translations.day4, $translations.day5, $translations.day6]; - let tabs = $state(selectedMeeting - ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 - ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsMeetingTimes, $translations.tabsOther, $translations.tabsChanges] - : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] - : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther]); - let tabsSnippets = $state(selectedMeeting - ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 - ? [basicTabContent, locationTabContent, meetingTimesTabContent, otherTabContent, changesTabContent] - : [basicTabContent, locationTabContent, otherTabContent, changesTabContent] - : [basicTabContent, locationTabContent, otherTabContent]); + let tabs = $state( + selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsMeetingTimes, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther] + ); + let tabsSnippets = $state( + selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [basicTabContent, locationTabContent, meetingTimesTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent] + ); const globalSettings = settings; const seenNames = new SvelteSet(); const ignoredFormats = ['VM', 'HY', 'TC']; @@ -118,10 +122,10 @@ const [hours, minutes] = globalSettings.defaultDuration.split(':').map((part) => part.padStart(2, '0')); defaultDuration = hours + ':' + minutes; } - type GroupMember = { id_bigint?: number, day: number; startTime: string; duration: string; formatIds: [] }; + type GroupMember = { id_bigint?: number; day: number; startTime: string; duration: string; formatIds: [] }; let groupMembers: GroupMember[] = []; if (selectedMeeting?.membersOfGroup && selectedMeeting.membersOfGroup.length > 0) { - groupMembers = (selectedMeeting.membersOfGroup as Array<{ id_bigint?: number, day?: number; startTime?: string; duration?: string; formats?: [] }>).map((member) => ({ + groupMembers = (selectedMeeting.membersOfGroup as Array<{ id_bigint?: number; day?: number; startTime?: string; duration?: string; formats?: [] }>).map((member) => ({ id_bigint: member.id_bigint ?? undefined, day: member.day ?? 0, startTime: member.startTime ?? '12:00', @@ -172,7 +176,7 @@ ...Object.fromEntries(Object.entries(selectedMeeting.customFields).map(([key, value]) => [key, value ?? ''])) } : Object.fromEntries(globalSettings.customFields.map((field) => [field.name, ''])), - membersOfGroup: groupMembers, + membersOfGroup: groupMembers }; let latitude = $state(initialValues.latitude); let longitude = $state(initialValues.longitude); @@ -347,18 +351,23 @@ formatIds: yup.array().of(yup.number()), venueType: yup.number().oneOf(VALID_VENUE_TYPES).required(), temporarilyVirtual: yup.bool(), - day: yup.number().default(-1).when('membersOfGroup', { - is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, - then: (schema) => schema.integer().min(0).max(6).required($translations.dayErrorMessage), - otherwise: (schema) => schema.notRequired() - }), + day: yup + .number() + .default(-1) + .when('membersOfGroup', { + is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, + then: (schema) => schema.integer().min(0).max(6).required($translations.dayErrorMessage), + otherwise: (schema) => schema.notRequired() + }), startTime: yup .string() .default('') .when('membersOfGroup', { is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, - then: (schema) => schema.matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm - .required($translations.startTimeErrorMessage), + then: (schema) => + schema + .matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm + .required($translations.startTimeErrorMessage), otherwise: (schema) => schema.notRequired() }), duration: yup @@ -809,29 +818,31 @@ }); $effect(() => { setData('formatIds', formatIdsSelected); - membersOfGroup.forEach((member,i) => {setData('membersOfGroup.'+i+'.formatIds', member.formatIds)}); + membersOfGroup.forEach((member, i) => { + setData('membersOfGroup.' + i + '.formatIds', member.formatIds); + }); }); - function handleDeleteMember(i: number, setData: (d: any, v: any)=>void) { + function handleDeleteMember(i: number, setData: (d: any, v: any) => void) { if (membersOfGroup.length <= 1) return; membersOfGroup.splice(i, 1); setData('membersOfGroup', membersOfGroup); } function handleAdd() { - membersOfGroup.push({ day: 0, startTime: "12:00", duration: "01:30", formatIds:[] }); + membersOfGroup.push({ day: 0, startTime: '12:00', duration: '01:30', formatIds: [] }); setData('membersOfGroup', membersOfGroup); } function convertToGroup() { tabs.splice(2, 0, $translations.tabsMeetingTimes); tabsSnippets.splice(2, 0, meetingTimesTabContent); - membersOfGroup = [{ day: $data?.day, startTime: $data?.startTime, duration: $data?.duration, formatIds:[] }]; + membersOfGroup = [{ day: $data?.day, startTime: $data?.startTime, duration: $data?.duration, formatIds: [] }]; setData('membersOfGroup', membersOfGroup); } function getDuration(i: number): string { - return membersOfGroup[i].duration ?? "01:00"; + return membersOfGroup[i].duration ?? '01:00'; } - function setDuration(i: number, d: string, setData: (d: any, v: any)=>void) { + function setDuration(i: number, d: string, setData: (d: any, v: any) => void) { membersOfGroup[i].duration = d; - setData("membersOfGroup."+i+".duration", d); + setData('membersOfGroup.' + i + '.duration', d); } function isGroup(): boolean { return tabs.includes($translations.tabsMeetingTimes); @@ -890,13 +901,7 @@ {selectedMeeting.id}
{#if !isGroup()} - + {/if} + {/if}
@@ -950,35 +949,35 @@
{#if !isGroup()} -
-
- - - {#if $errors.startTime} - - {$errors.startTime} - - {/if} -
-
- {$translations.durationTitle} - setData('duration', d)} /> - {#if $errors.duration} - - {$errors.duration} - - {/if} +
+
+ + + {#if $errors.startTime} + + {$errors.startTime} + + {/if} +
+
+ {$translations.durationTitle} + setData('duration', d)} /> + {#if $errors.duration} + + {$errors.duration} + + {/if} +
-
{/if}
@@ -1240,7 +1239,7 @@
{/snippet} {#snippet meetingTimesTabContent()} -
+
@@ -1249,34 +1248,36 @@ {#if membersOfGroup[i]?.id_bigint} {/if} -
+
-
-
-
- - -
-
- {$translations.durationTitle} - setDuration(i,d,setData)} /> +
+
+
+ + +
+
+ {$translations.durationTitle} + setDuration(i, d, setData)} /> +
-
-
- -
+ +
+
@@ -1425,7 +1426,7 @@ {/snippet} - +
From 38ffd56932c19e89e6739a840ca75aace8999797 Mon Sep 17 00:00:00 2001 From: otrok7 <50595291+otrok7@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:43:54 +0200 Subject: [PATCH 05/13] small fix --- src/app/Repositories/MeetingRepository.php | 2 +- src/resources/js/components/MeetingEditForm.svelte | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/Repositories/MeetingRepository.php b/src/app/Repositories/MeetingRepository.php index 0bc1dea4a..78846cdbb 100644 --- a/src/app/Repositories/MeetingRepository.php +++ b/src/app/Repositories/MeetingRepository.php @@ -755,7 +755,7 @@ public function update(int $id, array $values): bool Meeting::create($mainValues); } } - MeetingData::query()->where('meetingid_bigint', $id)->where('lang_enum', $this->getRequestedLanguage())->delete(); + MeetingData::query()->where('meetingid_bigint', $id)->delete(); MeetingLongData::query()->where('meetingid_bigint', $id)->delete(); foreach ($dataValues as $fieldName => $fieldValue) { $t = $dataTemplates->get($fieldName); diff --git a/src/resources/js/components/MeetingEditForm.svelte b/src/resources/js/components/MeetingEditForm.svelte index 267d24f07..f11c5b00d 100644 --- a/src/resources/js/components/MeetingEditForm.svelte +++ b/src/resources/js/components/MeetingEditForm.svelte @@ -901,7 +901,7 @@ {selectedMeeting.id}
{#if !isGroup()} - + {/if} + {/if}
@@ -1244,7 +1244,7 @@ {$translations.addMeeting}
- {#each membersOfGroup as member, i} + {#each membersOfGroup as $_, i} {#if membersOfGroup[i]?.id_bigint} {/if} @@ -1269,7 +1269,7 @@
{#if !isGroup()} - + {/if} + {/if}
@@ -1244,7 +1244,7 @@ {$translations.addMeeting}
- {#each membersOfGroup as $_, i} + {#each membersOfGroup as _$, i} {#if membersOfGroup[i]?.id_bigint} {/if} @@ -1269,7 +1269,7 @@
+ {#if !isGroup()} + + {/if}
{/if} + {#if !selectedMeeting && !isGroup()} + + {/if}
@@ -847,7 +933,7 @@
- {#each timeZoneGroups as continent} {#each continent.values as timezone} @@ -863,10 +949,11 @@ {/if}
+ {#if !isGroup()}
- {#if $errors.day} {$errors.day} @@ -892,10 +979,11 @@ {/if}
+ {/if}
- {#if $errors.serviceBodyId} {$errors.serviceBodyId} @@ -942,12 +1030,11 @@ {/if}
{/snippet} - {#snippet locationTabContent()}
- {#if $errors.venueType} {$errors.venueType} @@ -1074,7 +1161,7 @@
{#if countiesAndSubProvincesChoices.length > 0} - {:else} {/if} @@ -1089,7 +1176,7 @@
{#if statesAndProvincesChoices.length > 0} - {:else} {/if} @@ -1152,7 +1239,59 @@
{/snippet} - +{#snippet meetingTimesTabContent()} +
+ +
+ {#each membersOfGroup as member, i} + {#if membersOfGroup[i]?.id_bigint} + + {/if} +
+
+
+
+
+ + +
+
+ {$translations.durationTitle} + setDuration(i,d,setData)} /> +
+
+
+
+ +
+
+ + + {#snippet children({ item, clear })} +
e.stopPropagation()} onkeydown={(e) => e.stopPropagation()} role="presentation"> + + {item.name} + +
+ {/snippet} +
+
+
+ {/each} +{/snippet} {#snippet otherTabContent()}
@@ -1286,7 +1425,7 @@ {/snippet} - +
diff --git a/src/resources/js/lang/de.ts b/src/resources/js/lang/de.ts index 8dd73b553..7d4305cc6 100644 --- a/src/resources/js/lang/de.ts +++ b/src/resources/js/lang/de.ts @@ -209,6 +209,7 @@ export const deTranslations = { tabsBasic: 'Basic', tabsChanges: 'Änderungen', tabsLocation: 'Standort', + tabsMeetingTimes: 'Meeting Zeiten', tabsOther: 'Andere', technicalDetails: 'Technische Details', time: 'Zeit', diff --git a/src/resources/js/lang/en.ts b/src/resources/js/lang/en.ts index 8780ad9a6..6ca0db845 100644 --- a/src/resources/js/lang/en.ts +++ b/src/resources/js/lang/en.ts @@ -263,6 +263,7 @@ export const enTranslations = { tabsBasic: 'Basic', tabsChanges: 'Changes', tabsLocation: 'Location', + tabsMeetingTimes: 'Meeting Times', tabsOther: 'Other', technicalDetails: 'Technical Details', time: 'Time', diff --git a/src/storage/api-docs/api-docs.json b/src/storage/api-docs/api-docs.json index 3a3342cf7..136865ae6 100644 --- a/src/storage/api-docs/api-docs.json +++ b/src/storage/api-docs/api-docs.json @@ -2489,6 +2489,18 @@ "type": "string", "example": "string" }, + "membersOfGroup": { + "type": "array", + "items": { + "type": "object", + "example": { + "day": "0", + "startTime": "19:00", + "duration": "01:30", + "formats": "[]" + } + } + }, "customFields": { "type": "object", "example": { From 0b1d905d12c026e10c3d097e6a64828f296bdb2d Mon Sep 17 00:00:00 2001 From: otrok7 <50595291+otrok7@users.noreply.github.com> Date: Tue, 21 Oct 2025 16:01:28 +0200 Subject: [PATCH 10/13] de-lint --- .../js/components/MeetingEditForm.svelte | 191 +++++++++--------- 1 file changed, 96 insertions(+), 95 deletions(-) diff --git a/src/resources/js/components/MeetingEditForm.svelte b/src/resources/js/components/MeetingEditForm.svelte index 3610cba78..267d24f07 100644 --- a/src/resources/js/components/MeetingEditForm.svelte +++ b/src/resources/js/components/MeetingEditForm.svelte @@ -40,16 +40,20 @@ let { selectedMeeting, serviceBodies, formats, onSaved, onDeleted }: Props = $props(); const daysOfWeek: string[] = [$translations.day0, $translations.day1, $translations.day2, $translations.day3, $translations.day4, $translations.day5, $translations.day6]; - let tabs = $state(selectedMeeting - ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 - ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsMeetingTimes, $translations.tabsOther, $translations.tabsChanges] - : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] - : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther]); - let tabsSnippets = $state(selectedMeeting - ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 - ? [basicTabContent, locationTabContent, meetingTimesTabContent, otherTabContent, changesTabContent] - : [basicTabContent, locationTabContent, otherTabContent, changesTabContent] - : [basicTabContent, locationTabContent, otherTabContent]); + let tabs = $state( + selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsMeetingTimes, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther, $translations.tabsChanges] + : [$translations.tabsBasic, $translations.tabsLocation, $translations.tabsOther] + ); + let tabsSnippets = $state( + selectedMeeting + ? selectedMeeting.membersOfGroup && selectedMeeting.membersOfGroup.length > 0 + ? [basicTabContent, locationTabContent, meetingTimesTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent, changesTabContent] + : [basicTabContent, locationTabContent, otherTabContent] + ); const globalSettings = settings; const seenNames = new SvelteSet(); const ignoredFormats = ['VM', 'HY', 'TC']; @@ -118,10 +122,10 @@ const [hours, minutes] = globalSettings.defaultDuration.split(':').map((part) => part.padStart(2, '0')); defaultDuration = hours + ':' + minutes; } - type GroupMember = { id_bigint?: number, day: number; startTime: string; duration: string; formatIds: [] }; + type GroupMember = { id_bigint?: number; day: number; startTime: string; duration: string; formatIds: [] }; let groupMembers: GroupMember[] = []; if (selectedMeeting?.membersOfGroup && selectedMeeting.membersOfGroup.length > 0) { - groupMembers = (selectedMeeting.membersOfGroup as Array<{ id_bigint?: number, day?: number; startTime?: string; duration?: string; formats?: [] }>).map((member) => ({ + groupMembers = (selectedMeeting.membersOfGroup as Array<{ id_bigint?: number; day?: number; startTime?: string; duration?: string; formats?: [] }>).map((member) => ({ id_bigint: member.id_bigint ?? undefined, day: member.day ?? 0, startTime: member.startTime ?? '12:00', @@ -172,7 +176,7 @@ ...Object.fromEntries(Object.entries(selectedMeeting.customFields).map(([key, value]) => [key, value ?? ''])) } : Object.fromEntries(globalSettings.customFields.map((field) => [field.name, ''])), - membersOfGroup: groupMembers, + membersOfGroup: groupMembers }; let latitude = $state(initialValues.latitude); let longitude = $state(initialValues.longitude); @@ -347,18 +351,23 @@ formatIds: yup.array().of(yup.number()), venueType: yup.number().oneOf(VALID_VENUE_TYPES).required(), temporarilyVirtual: yup.bool(), - day: yup.number().default(-1).when('membersOfGroup', { - is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, - then: (schema) => schema.integer().min(0).max(6).required($translations.dayErrorMessage), - otherwise: (schema) => schema.notRequired() - }), + day: yup + .number() + .default(-1) + .when('membersOfGroup', { + is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, + then: (schema) => schema.integer().min(0).max(6).required($translations.dayErrorMessage), + otherwise: (schema) => schema.notRequired() + }), startTime: yup .string() .default('') .when('membersOfGroup', { is: (membersOfGroup: GroupMember[]) => !membersOfGroup || membersOfGroup.length === 0, - then: (schema) => schema.matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm - .required($translations.startTimeErrorMessage), + then: (schema) => + schema + .matches(/^([0-1]\d|2[0-3]):([0-5]\d)$/) // HH:mm + .required($translations.startTimeErrorMessage), otherwise: (schema) => schema.notRequired() }), duration: yup @@ -809,29 +818,31 @@ }); $effect(() => { setData('formatIds', formatIdsSelected); - membersOfGroup.forEach((member,i) => {setData('membersOfGroup.'+i+'.formatIds', member.formatIds)}); + membersOfGroup.forEach((member, i) => { + setData('membersOfGroup.' + i + '.formatIds', member.formatIds); + }); }); - function handleDeleteMember(i: number, setData: (d: any, v: any)=>void) { + function handleDeleteMember(i: number, setData: (d: any, v: any) => void) { if (membersOfGroup.length <= 1) return; membersOfGroup.splice(i, 1); setData('membersOfGroup', membersOfGroup); } function handleAdd() { - membersOfGroup.push({ day: 0, startTime: "12:00", duration: "01:30", formatIds:[] }); + membersOfGroup.push({ day: 0, startTime: '12:00', duration: '01:30', formatIds: [] }); setData('membersOfGroup', membersOfGroup); } function convertToGroup() { tabs.splice(2, 0, $translations.tabsMeetingTimes); tabsSnippets.splice(2, 0, meetingTimesTabContent); - membersOfGroup = [{ day: $data?.day, startTime: $data?.startTime, duration: $data?.duration, formatIds:[] }]; + membersOfGroup = [{ day: $data?.day, startTime: $data?.startTime, duration: $data?.duration, formatIds: [] }]; setData('membersOfGroup', membersOfGroup); } function getDuration(i: number): string { - return membersOfGroup[i].duration ?? "01:00"; + return membersOfGroup[i].duration ?? '01:00'; } - function setDuration(i: number, d: string, setData: (d: any, v: any)=>void) { + function setDuration(i: number, d: string, setData: (d: any, v: any) => void) { membersOfGroup[i].duration = d; - setData("membersOfGroup."+i+".duration", d); + setData('membersOfGroup.' + i + '.duration', d); } function isGroup(): boolean { return tabs.includes($translations.tabsMeetingTimes); @@ -890,13 +901,7 @@ {selectedMeeting.id}
{#if !isGroup()} - + {/if} + {/if}
@@ -950,35 +949,35 @@
{#if !isGroup()} -
-
- - - {#if $errors.startTime} - - {$errors.startTime} - - {/if} -
-
- {$translations.durationTitle} - setData('duration', d)} /> - {#if $errors.duration} - - {$errors.duration} - - {/if} +
+
+ + + {#if $errors.startTime} + + {$errors.startTime} + + {/if} +
+
+ {$translations.durationTitle} + setData('duration', d)} /> + {#if $errors.duration} + + {$errors.duration} + + {/if} +
-
{/if}
@@ -1240,7 +1239,7 @@
{/snippet} {#snippet meetingTimesTabContent()} -
+
@@ -1249,34 +1248,36 @@ {#if membersOfGroup[i]?.id_bigint} {/if} -
+
-
-
-
- - -
-
- {$translations.durationTitle} - setDuration(i,d,setData)} /> +
+
+
+ + +
+
+ {$translations.durationTitle} + setDuration(i, d, setData)} /> +
-
-
- -
+ +
+
@@ -1425,7 +1426,7 @@ {/snippet} - +
From 91f82eb6f421dc2364bf5687645ca8820aa1af4f Mon Sep 17 00:00:00 2001 From: otrok7 <50595291+otrok7@users.noreply.github.com> Date: Tue, 21 Oct 2025 17:43:54 +0200 Subject: [PATCH 11/13] small fix --- src/app/Repositories/MeetingRepository.php | 2 +- src/resources/js/components/MeetingEditForm.svelte | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/Repositories/MeetingRepository.php b/src/app/Repositories/MeetingRepository.php index 0bc1dea4a..78846cdbb 100644 --- a/src/app/Repositories/MeetingRepository.php +++ b/src/app/Repositories/MeetingRepository.php @@ -755,7 +755,7 @@ public function update(int $id, array $values): bool Meeting::create($mainValues); } } - MeetingData::query()->where('meetingid_bigint', $id)->where('lang_enum', $this->getRequestedLanguage())->delete(); + MeetingData::query()->where('meetingid_bigint', $id)->delete(); MeetingLongData::query()->where('meetingid_bigint', $id)->delete(); foreach ($dataValues as $fieldName => $fieldValue) { $t = $dataTemplates->get($fieldName); diff --git a/src/resources/js/components/MeetingEditForm.svelte b/src/resources/js/components/MeetingEditForm.svelte index 267d24f07..f11c5b00d 100644 --- a/src/resources/js/components/MeetingEditForm.svelte +++ b/src/resources/js/components/MeetingEditForm.svelte @@ -901,7 +901,7 @@ {selectedMeeting.id}
{#if !isGroup()} - + {/if} + {/if}
@@ -1244,7 +1244,7 @@ {$translations.addMeeting}
- {#each membersOfGroup as member, i} + {#each membersOfGroup as $_, i} {#if membersOfGroup[i]?.id_bigint} {/if} @@ -1269,7 +1269,7 @@
{#if !isGroup()} - + {/if} + {/if}
@@ -1244,7 +1244,7 @@ {$translations.addMeeting}
- {#each membersOfGroup as $_, i} + {#each membersOfGroup as _$, i} {#if membersOfGroup[i]?.id_bigint} {/if} @@ -1269,7 +1269,7 @@
{#if !isGroup()} - + {/if} + {/if}
@@ -932,7 +930,7 @@
- {#each timeZoneGroups as continent} {#each continent.values as timezone} @@ -952,7 +950,7 @@
- {#if $errors.day} {$errors.day} @@ -982,7 +980,7 @@
- {#if $errors.serviceBodyId} {$errors.serviceBodyId} @@ -1033,7 +1031,7 @@
- {#if $errors.venueType} {$errors.venueType} @@ -1160,7 +1158,7 @@
{#if countiesAndSubProvincesChoices.length > 0} - {:else} {/if} @@ -1175,7 +1173,7 @@
{#if statesAndProvincesChoices.length > 0} - {:else} {/if} @@ -1244,7 +1242,7 @@ {$translations.addMeeting}
- {#each membersOfGroup as _$, i} + {#each membersOfGroup as _, i} {#if membersOfGroup[i]?.id_bigint} {/if} @@ -1254,7 +1252,7 @@
-
@@ -1269,7 +1267,7 @@