<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Karolina Ruda]]></title><description><![CDATA[Zagłębiam się w świat Power Platform i dzielę się swoimi doświadczeniami, tworząc praktyczne i kreatywne rozwiązania.]]></description><link>https://karolinaruda.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 05 Oct 2026 12:25:49 GMT</lastBuildDate><atom:link href="https://karolinaruda.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Jak z pomocą Power Automate, Power Apps, SharePoint i Azure AI Document Intelligence stworzyłam rozwiązanie do analizowania faktur]]></title><description><![CDATA[Faktura to nieodłączny element działalności każdej firmy – niezależnie od branży czy skali biznesu. Niestety, w wielu organizacjach obieg faktur wciąż opiera się na ręcznym wprowadzaniu danych, co jest czasochłonne i podatne na błędy. W tej sytuacji ...]]></description><link>https://karolinaruda.hashnode.dev/jak-z-pomoca-power-automate-power-apps-sharepoint-i-azure-ai-document-intelligence-stworzylam-rozwiazanie-do-analizowania-faktur</link><guid isPermaLink="true">https://karolinaruda.hashnode.dev/jak-z-pomoca-power-automate-power-apps-sharepoint-i-azure-ai-document-intelligence-stworzylam-rozwiazanie-do-analizowania-faktur</guid><category><![CDATA[Power Platform]]></category><category><![CDATA[power-automate]]></category><category><![CDATA[SharePoint]]></category><category><![CDATA[SharePoint Online]]></category><category><![CDATA[Document Intelligence]]></category><category><![CDATA[OCR ]]></category><category><![CDATA[azure ai services]]></category><category><![CDATA[Azure AI Foundry]]></category><category><![CDATA[invoice automation]]></category><dc:creator><![CDATA[Karolina Ruda]]></dc:creator><pubDate>Tue, 02 Sep 2025 10:37:29 GMT</pubDate><content:encoded><![CDATA[<p>Faktura to nieodłączny element działalności każdej firmy – niezależnie od branży czy skali biznesu. Niestety, w wielu organizacjach obieg faktur wciąż opiera się na ręcznym wprowadzaniu danych, co jest czasochłonne i podatne na błędy. W tej sytuacji z pomocą przychodzi usługa Azure AI Document Intelligence, która umożliwia odczytywanie kluczowych informacji z dokumentów.</p>
<p>W tym wpisie chciałbym przedstawić jak z pomocą narzędzi Power Platform, SharePoint oraz usługi Document Inteligence zbudowałam kompletne rozwiązanie umożliwiające w pełni zautomatyzować proces obsługi faktur - od dodania faktury, przez analizę dokumentu, aż po zapisanie danych w SharePoint.</p>
<h2 id="heading-1-scenariusz-przetwarzania-faktur"><strong>1.</strong> Scenariusz przetwarzania faktur</h2>
<p>Załóżmy, że w naszej firmie proces obsługi faktur wygląda standardowo – użytkownik otrzymuje dokument, wprowadza dane ręcznie do systemu, a następnie księgowość weryfikuje poprawność i status każdej faktury. Jest to czasochłonne, podatne na błędy i trudne do monitorowania, szczególnie gdy liczba dokumentów rośnie.</p>
<p>Aby usprawnić cały proces, możemy rozłożyć go na kilka kluczowych kroków, które zostaną w pełni zautomatyzowane przy użyciu Power Platform, SharePoint i Azure Document Intelligence:</p>
<ol>
<li><p>Użytkownik dodaje fakturę przez aplikację Power Apps.</p>
</li>
<li><p>Power Automate zapisuje dokument w bibliotece SharePoint i tworzy nowy rekord na liście.</p>
</li>
<li><p>Dokument trafia do Azure Document Intelligence, gdzie zostaje przeanalizowany.</p>
</li>
<li><p>Wynik analizy wraca w formacie JSON i jest parsowany w Power Automate.</p>
</li>
<li><p>Rekord w SharePoint zostaje uzupełniony o rozpoznane dane, a jego status zmienia się na “Processed”.</p>
</li>
<li><p>W przypadku błędu, status elementu aktualizuje się na “Error”, co pozwala szybko wychwycić nieprawidłowości.</p>
</li>
</ol>
<h2 id="heading-2-elementy-rozwiazania">2. <strong>Elementy rozwiązania</strong></h2>
<p>Rozwiązanie zostało zbudowane na kilku elementach ekosystemu Microsoft.</p>
<h3 id="heading-21-power-apps-canvas-app">2.1. Power Apps (Canvas App)</h3>
<p>Power Apps pełni rolę interfejsu wejściowego – w naszej aplikacji użytkownik wprowadza fakturę, a po zatwierdzeniu pliku uruchamiany jest przepływ w Power Automate.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756724952746/ba03219a-e3d7-4f7f-803b-e62753516e00.png" alt class="image--center mx-auto" /></p>
<pre><code class="lang-plaintext">If(
    CountRows(AttachInvoice.Attachments) = 0,
    Notify(
        "Wybierz plik przed zatwierdzeniem.",
        NotificationType.Error
    ),
    Set(
        _att,
        First(AttachInvoice.Attachments)
    );
    SaveInvoiceAndAnalyze.Run(
        {
            name: _att.Name,
            contentBytes: _att.Value
        }
    );
    Notify(
        "Faktura została przesłana do systemu.",
        NotificationType.Success
    );
    Reset(AttachInvoice);

)
</code></pre>
<p>Warto podkreślić, że w tym projekcie Power Apps pełni jedynie rolę symulacji zewnętrznego źródła danych – w rzeczywistych scenariuszach ten krok mógłby zostać zastąpiony innym systemem, np. platformą ERP, systemem CRM, dedykowanym portalem dla klientów czy nawet zewnętrzną aplikacją integrującą się przez API. Dzięki temu rozwiązanie pozostaje elastyczne i można je dostosować do różnych potrzeb biznesowych.</p>
<h3 id="heading-22-sharepoint">2.2. SharePoint</h3>
<p>SharePoint pełni dwie funkcje w rozwiązaniu:</p>
<ul>
<li><p>Biblioteka dokumentów „DokumentyFaktur” – tutaj przechowywane są wszystkie przesłane faktury w formie plików.</p>
</li>
<li><p>Lista „Faktury” – służy do przechowywania danych faktur. Lista zawiera kolumny takie jak:</p>
<ul>
<li><p>Title - tytuł rekordu</p>
</li>
<li><p>InvoiceNumber - numer faktury</p>
</li>
<li><p>IssuedTo - osoba dla której wystawiono fakturę</p>
</li>
<li><p>DueDate - termin płatności faktury</p>
</li>
<li><p>Tax - wartość podatku</p>
</li>
<li><p>Total - łączna wartość faktury</p>
</li>
<li><p>Status - pole Choice określające status - “New“ (wartość domyślna, nadawana w momencie utworzenia rekordu faktury), “Processed“ (wartość nadawana po przetworzeniu faktury), “Error“ (wartość nadawana w momencie błędu przy przetwarzaniu faktury)</p>
</li>
<li><p>FileLink - link do pliku faktury w bibliotece “DokumentyFaktur“</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756809760580/7685d5f7-dc43-407a-adcf-6b919c49721e.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-23-power-automate">2.3. Power Automate</h3>
<p>Power Automate pełni rolę centrum całego procesu. Flow odpowiada za:</p>
<ul>
<li><p>zapisanie pliku w bibliotece SharePoint,</p>
</li>
<li><p>utworzenie rekordu na liście faktur,</p>
</li>
<li><p>przesłanie dokumentu do usługi Azure Document Intelligence,</p>
</li>
<li><p>parsowanie zwróconego JSON-a i wyciągnięcie kluczowych wartości (numer faktury, termin płatności, kwoty itp.),</p>
</li>
<li><p>aktualizację rekordu w SharePoint z odpowiednim statusem (Processed lub Error).</p>
</li>
</ul>
<p>Dzięki Power Automate proces przebiega w pełni automatycznie, a każdy rekord faktury ma przypisany aktualny status i powiązany plik w bibliotece.</p>
<h3 id="heading-24-azure-ai-document-intelligence">2.4. Azure AI Document Intelligence</h3>
<p>Usługa Document Intelligence odpowiada za automatyczną analizę zawartości dokumentu. W moim projekcie wykorzystałam własny model trenowany na przykładowych fakturach, który umożliwia odczytywanie:</p>
<ul>
<li><p>numeru faktury,</p>
</li>
<li><p>osoby dla której faktura została wystawiona</p>
</li>
<li><p>terminu płatności,</p>
</li>
<li><p>wartości podatku i kwoty całkowitej.</p>
</li>
</ul>
<h2 id="heading-3-custom-model-w-document-intelligence">3. Custom model w Document Intelligence</h2>
<p>Azure Document Intelligence to narzędzie oparte na sztucznej inteligencji, które umożliwia automatyczne odczytywanie danych z dokumentów, takich jak faktury, paragony czy formularze. Usługa pozwala na szybkie przetwarzanie dokumentów i wyciąganie kluczowych informacji w formacie JSON, gotowych do dalszego wykorzystania w systemach biznesowych.</p>
<p>Jednym z najciekawszych elementów Document Intelligence jest możliwość tworzenia własnych, niestandardowych modeli (Custom Models). Dzięki nim możemy trenować usługę na przykładach dokumentów charakterystycznych dla naszej firmy, aby dokładnie rozpoznawała potrzebne pola. Model uczy się na podstawie oznaczonych przykładowych dokumentów i potrafi automatycznie wyciągać informacje, takie jak numer faktury, datę wystawienia, kwotę netto i brutto czy termin płatności.</p>
<p>Na potrzeby projektu stworzyłam model opierający się na fakturach z poniższego szablonu.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756731742044/93ffccb1-bc85-47fe-9b37-53b25989f49f.jpeg" alt class="image--center mx-auto" /></p>
<p>Przyjęłam, że wartości które będą istotne do analizy to numer faktury, dane osoby dla której faktura została wystawiona, data płatności, wartość podatku oraz pełna kwota faktury. W oparciu o te założenia przygotowałam własny model <strong>invoice-processor</strong>, w którym utworzyłam pola: InvoiceNumber, IssuedTo, DueDate, Tax oraz GrandTotal. Następnie oznaczyłam odpowiednie wartości w przykładowych dokumentach i wytrenowałam model, tak aby potrafił samodzielnie rozpoznawać je w nowych fakturach.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756739109455/72e2f4a5-4329-4e06-9bdf-aa901cf1caa6.png" alt class="image--center mx-auto" /></p>
<p>Do wytrenowania niestandardowego modelu w Azure Document Intelligence wymagane jest przygotowanie co najmniej 5 przykładowych dokumentów zawierających pola, które chcemy rozpoznawać. Microsoft zaleca jednak użycie 20–50 dokumentów, aby model mógł lepiej zrozumieć różne układy i warianty dokumentów. Zasada jest prosta – im większy i bardziej zróżnicowany zestaw treningowy, tym dokładniejsze i bardziej odporne na błędy będą wyniki analizy.</p>
<p>Sam proces opiera się na algorytmach uczenia maszynowego – model uczy się, jak rozpoznawać oznaczone przez nas pola, a następnie generalizuje tę wiedzę, aby radzić sobie z nowymi dokumentami o podobnej strukturze. Dzięki temu po wytrenowaniu jest w stanie automatycznie wyciągać wymagane informacje z faktur, nawet jeśli różnią się one wizualnie od przykładów użytych podczas treningu.</p>
<p>Warto wspomnieć, że usługa Azure Document Intelligence jest dostępna (choć z pewnymi ograniczeniami) również w ramach darmowego konta Azure. Dzięki temu, korzystając jedynie z konta deweloperskiego, mogłam zbudować rozwiązanie, które wykorzystuje zaawansowane procesy oparte na sztucznej inteligencji. To świetna opcja dla osób, które chcą przetestować możliwości narzędzia, stworzyć prototyp lub po prostu sprawdzić, jak automatyzacja obiegu dokumentów może usprawnić pracę w ich organizacji.</p>
<h2 id="heading-4-szczegolowy-przebieg-flow">4. Szczegółowy przebieg Flow</h2>
<p>Aby lepiej zrozumieć, jak działa całe rozwiązanie, warto przyjrzeć się procesowi krok po kroku.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756727327292/b956330e-8386-4b02-94d3-7b1e43a9c686.png" alt class="image--center mx-auto" /></p>
<ol>
<li><p>Przepływ rozpoczyna akcja <strong>When Power Apps Calls a flow</strong> która przekazuje parametr <strong>File content</strong> - w tym przypadku plik przesłany przez użytkownika w aplikacji Power Apps.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756727005071/07625ffc-d1ac-403d-b517-4a47128e8eff.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Następną akcją jest <strong>Create file</strong> gdzie w bibliotece “DokumentyFaktur“ tworzony jest przesłany plik faktury.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756727393283/fb280313-7997-487d-b41d-df5f8daa6e9a.png" alt class="image--center mx-auto" /></p>
<p> Wartość File Name została określona w wyrażeniu które generuje unikalną nazwę dla każdej faktury, w oparciu o datę i godzinę przesłania dokumentu.</p>
<pre><code class="lang-plaintext"> concat(
   'Invoice_', 
   formatDateTime(
     convertTimeZone(utcNow(), 'UTC', 'W. Europe Standard Time'), 
     'dd-MM-yyyy_HH_mm_ss'
   ), 
   '.pdf'
 )
</code></pre>
</li>
<li><p>Kolejnym krokiem jest akcja <strong>Get file properties</strong> która pobiera dane utworzonego pliku faktury, które będą nam potrzebne w kolejnym kroku.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756727987794/9e5fe0bf-cda3-4d1e-bddc-c79cd0cf47e8.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Następnie w akcji <strong>Create item</strong> tworzymy nowy element listy “Faktury” z uwzględnieniem wartości takich jak “Title“ - odpowiadającego nazwie pliku bez rozszerzenia “.pdf“, “Status Value” ustawionym jako “New“ oraz “FileLink“ ustawionym jako adres do pliku faktury.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756728124402/e5b45f39-449c-4e07-9b3b-6867aca56d69.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Kolejną akcją jest <strong>Analyze Document for Prebuilt or Custom models</strong> w której przekazywana jest między innymi nazwa modelu z którego chcemy korzystać przy analizie dokumentu. W tym przypadku wybrany został custom model <strong>invoice-processor</strong>, stworzony na potrzeby projektu. Kolejnym istotnym parametrem jest Document/Image File Content, który stanowi przesłany plik.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756728716933/5754f6f2-0435-41f3-9132-e02428f7b64c.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Po przeanalizowaniu dokumentu kolejnym etapem we flow jest <strong>Scope – Try</strong>, którego celem jest bezpieczne przetwarzanie wyników analizy i aktualizacja danych na liście SharePoint.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756729932925/a6f1b17d-cc20-4dde-ae2a-76599a2fcfa3.png" alt class="image--center mx-auto" /></p>
<p> Do jego elementów należą:</p>
<ul>
<li><p>Akcja <strong>Compose</strong> która pobiera zwrócone Body po analizie dokumentu.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756729656115/8b669c0a-2f2c-433c-a2f7-dff6731c8ddc.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Akcja <strong>Parse JSON</strong> która służy do przetwarzania danych JSON i umożliwia łatwe wyciąganie poszczególnych wartości potrzebnych w kolejnych krokach.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756729734736/88e0e651-3a9d-41e1-8ce7-cf892b6a5de0.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Akcje <strong>Compose InvoiceNumber, Compose IssuedTo, Compose DueDate, ComposeTax, ComposeTotal</strong>, które pobierają poszczególne elementy ze zwróconej analizy dokumentu. Ułatwiają one dostęp do interesujących nas danych potrzebnych do aktualizacji elementu listy.</p>
<p>  Poniżej przykładowe wyrażenie pobierające wartość “InvoiceNumber“ z przeprowadzonej analizy.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756730301978/a5b0b8e5-2351-46cb-bfa7-7ff767fd58f0.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Akcja <strong>Update Item</strong> w której aktualizujemy element listy o dane otrzymane z analizy pliku, z uwzględnieniem prawidłowego formatu.</p>
</li>
</ul>
</li>
</ol>
<p>    <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756730486553/37aeef89-5c56-41eb-a9ab-f6027328aba7.png" alt class="image--center mx-auto" /></p>
<ol start="7">
<li><p>Dodatkowo stworzony został <strong>Scope - Catch</strong>, który uruchamia się w przypadku niepowodzenia wykonania Scope - Try. Pełni on funkcję obsługi błędów.</p>
<p> <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756730701514/e3e1bb55-e6a2-470e-a426-ecf71e528e6b.png" alt class="image--center mx-auto" /></p>
</li>
</ol>
<p>Została w nim zawarta akcja <strong>Update item on Fail</strong> w której status elementu listy ustawiany jest na wartość “Error“.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756730872093/df99d53a-e056-4036-b408-146a3e41750b.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-5-rezultaty">5. Rezultaty</h2>
<p>Nic nie oddaje działania aplikacji lepiej niż przykład w praktyce. Poniżej przedstawiam dwa scenariusze obrazujące rezultat całego procesu:</p>
<ul>
<li><p>poprawne dodanie faktury, które kończy się analizą dokumentu i uzupełnieniem danych w SharePoint,</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756808136932/06912aca-633f-4bb0-b5ed-4a347eb0d498.gif" alt class="image--center mx-auto" /></p>
</li>
<li><p>oraz sytuację, w której wystąpił błąd – a status faktury został automatycznie zmieniony na <em>Error</em>.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756808838315/0ef10061-c23d-4f8c-a107-d146f519bd12.gif" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h2 id="heading-6-podsumowanie">6. Podsumowanie</h2>
<p>W tym artykule pokazałam, jak w pełni zautomatyzować proces obsługi faktur przy użyciu narzędzi z rodziny Microsoft. Udało się zbudować rozwiązanie, które automatycznie przetwarza faktury, zapisuje dane w SharePoint oraz obsługuje ewentualne błędy. Stworzenie takiego procesu nie wymaga dużego nakładu czasu ani zaawansowanej wiedzy programistycznej, a może znacząco usprawnić obieg dokumentów w firmie i pozwolić zespołowi skupić się na innych zadaniach, zyskując dodatkowe zasoby w codziennej pracy.</p>
]]></content:encoded></item><item><title><![CDATA[Jak stworzyłam model-driven app do zarządzania wydarzeniami w organizacji]]></title><description><![CDATA[Model-driven apps w Power Apps to rozwiązania oparte na danych, które pozwalają tworzyć zaawansowane aplikacje biznesowe bez konieczności pisania dużej ilości kodu. Bazują na strukturze danych w Dataverse oraz umożliwiają między innymi zarządzanie re...]]></description><link>https://karolinaruda.hashnode.dev/jak-stworzylam-model-driven-app-do-zarzadzania-wydarzeniami-w-organizacji</link><guid isPermaLink="true">https://karolinaruda.hashnode.dev/jak-stworzylam-model-driven-app-do-zarzadzania-wydarzeniami-w-organizacji</guid><category><![CDATA[timer trigger]]></category><category><![CDATA[PowerPlatform]]></category><category><![CDATA[model driven app]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Power Apps]]></category><category><![CDATA[Dataverse]]></category><category><![CDATA[microsoft power platform]]></category><category><![CDATA[microsoft powerapps]]></category><category><![CDATA[Azure Functions]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Karolina Ruda]]></dc:creator><pubDate>Thu, 28 Aug 2025 08:19:25 GMT</pubDate><content:encoded><![CDATA[<p>Model-driven apps w Power Apps to rozwiązania oparte na danych, które pozwalają tworzyć zaawansowane aplikacje biznesowe bez konieczności pisania dużej ilości kodu. Bazują na strukturze danych w Dataverse oraz umożliwiają między innymi zarządzanie rekordami, tworzenie formularzy, widoków, dashboardów, implementowanie logiki biznesowej i uprawnień użytkowników.</p>
<p>Projekt opisany we wpisie to aplikacja <strong>EventFlow</strong>, której celem jest kompleksowe zarządzanie wydarzeniami w organizacji.</p>
<h2 id="heading-1-zalozenia-projektu">1. <strong>Założenia projektu</strong></h2>
<p>Celem projektu jest stworzenie profesjonalnej aplikacji model-driven, która umożliwi pracownikom firmy:</p>
<ul>
<li><p>Zarządzanie wydarzeniami firmowymi (np. szkolenia, konferencje, integracje).</p>
</li>
<li><p>Rejestrację uczestników i śledzenie frekwencji w odniesieniu do dostępnej liczby miejsc wydarzenia.</p>
</li>
<li><p>Dodawanie anonimowych ocen po zakończeniu wydarzenia.</p>
</li>
<li><p>Wizualizację danych dotyczących wydarzeń, uczestników i ocen w widokach tabel i dashboardach.</p>
</li>
</ul>
<p>Tworząc aplikację należy pamiętać o następujących założeniach:</p>
<ol>
<li><p>Uprawnienia użytkowników</p>
<ul>
<li><p>Zwykły użytkownik może zapisać się na wydarzenie, dodać ocenę wydarzenia oraz przeglądać listę wydarzeń.</p>
</li>
<li><p>Tylko użytkownicy z rolą Organizatora mogą tworzyć i zarządzać wydarzeniami.</p>
</li>
</ul>
</li>
<li><p>Daty wydarzenia</p>
<ul>
<li><p>Wydarzenie nie może zostać utworzone z datą wsteczną.</p>
</li>
<li><p>Data zakończenia wydarzenia musi być późniejsza niż data jego rozpoczęcia.</p>
</li>
</ul>
</li>
<li><p>Statusy wydarzeń</p>
<ul>
<li><p>Każde wydarzenie może znajdować się w jednym z następujących statusów: “Planned”, “Registration”, “Full”, “In Progress”, “Completed”.</p>
</li>
<li><p>Status “Full” ustawiany jest automatycznie, gdy wydarzenie znajduje się w fazie “Registration” i liczba dostępnych miejsc wynosi 0.</p>
</li>
<li><p>Status “In Progress” ustawiany jest automatycznie, gdy bieżąca data mieści się w przedziale między datą rozpoczęcia a datą zakończenia wydarzenia.</p>
</li>
<li><p>Status “Completed” ustawiany jest automatycznie po upływie daty i godziny zakończenia wydarzenia.</p>
</li>
</ul>
</li>
<li><p>Dostępność miejsc wydarzenia</p>
<ul>
<li><p>Każde wydarzenie ma określoną maksymalną liczbę miejsc.</p>
</li>
<li><p>Liczba dostępnych miejsc wyliczana jest automatycznie na podstawie całkowitej pojemności oraz liczby zapisanych uczestników.</p>
</li>
<li><p>Użytkownik może zapisać się tylko na wydarzenie w statusie “Registration” i jedynie wtedy, gdy liczba dostępnych miejsc jest większa od zera.</p>
</li>
</ul>
</li>
<li><p>Oceny wydarzeń</p>
<ul>
<li>Dodanie oceny do wydarzenia możliwe jest wyłącznie po jego zakończeniu (status “Completed”).</li>
</ul>
</li>
</ol>
<h2 id="heading-2-tabele-i-relacje">2. Tabele i relacje</h2>
<p>Sercem każdej aplikacji model-driven są dane przechowywane w Dataverse. Dataverse to nowoczesna platforma do przechowywania danych w chmurze Microsoft, która umożliwia tworzenie tabel, definiowanie atrybutów, relacji oraz logiki biznesowej. Aplikacje model-driven mogą w prosty sposób korzystać z tych danych, prezentować je w widokach, formularzach i dashboardach oraz integrować z innymi aplikacjami i usługami w ekosystemie Microsoft Power Platform.</p>
<h3 id="heading-21-tabela-event">2.1. Tabela Event</h3>
<p>Ta tabela zawiera wszystkie informacje dotyczące wydarzeń w firmie. Zawiera kolumny takie jak:</p>
<ul>
<li><p><strong>Event Indicator</strong> - (Text) Primary Name Column, nazwa wydarzenia + data, rekord służący do wyszukiwania wydarzenia w Lookupach (wymagane)</p>
</li>
<li><p><strong>Event Name</strong> - (Text) nazwa wydarzenia (wymagane)</p>
</li>
<li><p><strong>Start Date</strong> - (Date and time) początek wydarzenia (wymagane)</p>
</li>
<li><p><strong>End Date</strong> - (Date and time) koniec wydarzenia (wymagane)</p>
</li>
<li><p><strong>Capacity</strong> - (Whole number) maksymalna liczba uczestników wydarzenia (wymagane)</p>
</li>
<li><p><strong>Event Status</strong> - (Choice) status wydarzenia: “Planned“, “Registration“, “Full“, “In Progress“, “Completed“ (wymagane)</p>
</li>
<li><p><strong>Organizer</strong> - (Lookup do systemowej tabeli Contact) osoba organizująca wydarzenie (wymagane)</p>
</li>
<li><p><strong>Available Seats</strong> - (Whole number - Calculated: Capacity - Number of Participants) dostępna liczba miejsc do zapisania</p>
</li>
<li><p><strong>Average Rating</strong> - (Decimal - Rollup: średnia Feedback.Rating dla powiązanych rekordów tabeli Event) średnia ocen</p>
</li>
<li><p><strong>Number Of Participants</strong> - (Whole number - Rollup: Policz powiązane rekordy z tabeli Participant gdzie Registration Status ≠ <em>Cancelled</em>) liczba zapisanych uczestników</p>
</li>
<li><p><strong>Number Of Responses</strong> - (Whole number - Rollup: Policz powiązane rekordy z tabeli Feedback) liczba wystawionych ocen</p>
</li>
</ul>
<h3 id="heading-22-tabela-participant">2.2. Tabela Participant</h3>
<p>Ta tabela zawiera wszystkie informacje na temat uczestników wydarzeń. Zawiera kolumny takie jak:</p>
<ul>
<li><p><strong>First Name</strong> - (Text) imię uczestnika (wymagane)</p>
</li>
<li><p><strong>Last Name</strong> - (Text) nazwisko uczestnika (wymagane)</p>
</li>
<li><p><strong>Email</strong> - (Email) adres email uczestnika (wymagane)</p>
</li>
<li><p><strong>Phone</strong> - (Phone number) telefon kontaktowy uczestnika (wymagane)</p>
</li>
<li><p><strong>Registration Status</strong> - (Choice) status deklaracji uczestnictwa: “Registered“, “Confirmed“, “Canceled“ (wymagane)</p>
</li>
<li><p><strong>Event</strong> - (Lookup do tabeli Event) wydarzenie na które zapisuje się uczestnik (wymagane)</p>
</li>
</ul>
<h3 id="heading-23-tabela-feedback">2.3. Tabela Feedback</h3>
<p>Ta tabela zawiera wszystkie informacje na temat anonimowych ocen wydarzeń. Zawiera kolumny takie jak:</p>
<ul>
<li><p><strong>Rating</strong> - (Whole number) ocena wydarzenia od 1 do 5 (wymagane)</p>
</li>
<li><p><strong>Comment</strong> - (Text) komentarz do oceny (opcjonalne)</p>
</li>
<li><p><strong>Event</strong> - (Lookup do tabeli Event) oceniane wydarzenie (wymagane)</p>
</li>
</ul>
<h3 id="heading-24-relacje-miedzy-tabelami">2.4. Relacje między tabelami</h3>
<p>Tabele w aplikacji są ze sobą ściśle powiązane, co umożliwia tworzenie spójnego modelu danych:</p>
<ul>
<li><p><strong>Event (1:N) Participant</strong> – jedno wydarzenie może mieć wielu uczestników, uczestnik należy zawsze do jednego wydarzenia.</p>
</li>
<li><p><strong>Event (1:N) Feedback</strong> – jedno wydarzenie może mieć wiele ocen, ocena zawsze przypisana jest do jednego wydarzenia.</p>
</li>
</ul>
<p>Dzięki temu model danych jest prosty, a jednocześnie pozwala na:</p>
<ul>
<li><p>monitorowanie frekwencji,</p>
</li>
<li><p>kontrolę dostępnych miejsc,</p>
</li>
<li><p>analizę ocen i rankingów wydarzeń.</p>
</li>
</ul>
<h2 id="heading-3-widoki">3. Widoki</h2>
<p>W aplikacjach model-driven bardzo ważnym elementem są widoki (Views), które pozwalają użytkownikowi szybko przeglądać i analizować dane. W projekcie EventFlow przygotowałam zestaw widoków dla każdej z tabel.</p>
<h3 id="heading-31-widoki-tabeli-event">3.1. Widoki tabeli Event</h3>
<ul>
<li><p><strong>Upcoming Events</strong> – prezentuje nadchodzące wydarzenia, które mają status Planned, Registration lub Full sortowane po dacie rozpoczęcia wydarzenia rosnąco.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756226646284/bbfea736-7854-4326-9825-5f59dded471d.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Completed Events</strong> – pokazuje tylko zakończone wydarzenia, które mają status Completed. Widok posortowany jest według daty zakończenia malejąco.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756226719067/256377ca-47a3-4ee4-bf5d-b3e259402ecb.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Events In Progress</strong> - pokazuje tylko te wydarzenia, które mają status In Progress.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756631753871/b3b69741-8364-45ef-bccb-d15430206a52.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Events - All</strong> – pełny katalog wszystkich wydarzeń, który jest domyślnym widokiem tabeli Event.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756226905770/a7cb7650-d9ee-457f-8081-540a2907e252.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h3 id="heading-32-widoki-tabeli-participant">3.2. Widoki tabeli Participant</h3>
<ul>
<li><p><strong>Participants – Active</strong> – pokazuje wszystkich uczestników z aktywnym statusem rejestracji (Registered, Confirmed).</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756227026683/7b5734ff-389c-4dec-8657-a4045bade72f.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Participants - All</strong> – pełny katalog wszystkich uczestników, niezależnie od statusu rejestracji. Domyślny widok dla tabeli Participant.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756227118696/478dfdfa-8b0e-44d2-a67a-4ff04452e8c6.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h3 id="heading-33-widoki-tabeli-feedback">3.3. Widoki tabeli Feedback</h3>
<ul>
<li><p><strong>Feedback – All</strong> – lista wszystkich ocen wystawionych w aplikacji. Domyślny widok dla tabeli Feedback.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756227427715/ab51cc1e-3a1e-491f-a514-12f961890e29.png" alt class="image--center mx-auto" /></p>
</li>
<li><p><strong>Feedback – By Event</strong> – widok pogrupowany według wydarzeń, co umożliwia szybkie porównanie ocen między poszczególnymi szkoleniami.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756227477646/a49c1fa4-0ce6-4863-a794-47a7e1b555eb.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h2 id="heading-4-formularze">4. Formularze</h2>
<p>Formularze w aplikacji model-driven pełnią kluczową rolę w pracy użytkowników – to właśnie one stanowią główny interfejs do wprowadzania, przeglądania i edycji danych zapisanych w tabelach Dataverse. Odpowiednio zaprojektowany formularz nie tylko pozwala na wprowadzanie i aktualizację rekordów, ale także prowadzi użytkownika przez proces biznesowy, ukrywając pola zbędne w danym kontekście i eksponując te, które są najważniejsze. Dzięki zakładkom, sekcjom, subgridom i regułom widoczności możliwe jest stworzenie interfejsu czytelnego, intuicyjnego i zgodnego z logiką aplikacji.</p>
<h3 id="heading-41-formularz-tabeli-event">4.1. Formularz tabeli Event</h3>
<p>Formularz podzielono na 4 sekcje:</p>
<ul>
<li><p>Event Details - zawiera podstawowe informacje dotyczące wydarzenia: Event Name, Organizer, Start Date, End Date, Capacity, Event Status.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756229600845/1ba9ac35-9d23-48ee-91ea-9accaf205f55.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Metrics - zawiera kluczowe wskaźniki liczbowe: Number Of Participants, Available Seats, Average Rating</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756229678538/a2f45511-960b-4eef-8f99-d3bdd5d4748d.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Participants - subgrid wskazujący uczestników przypisanych do danego wydarzenia ze statusem uczestnictwa innym niż “Cancelled”. Formularz zawiera funkcję “Add New” umożliwiającą na szybkie stworzenie nowego rekordu uczestnika dla wybranego wydarzenia.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756229760922/ae9c014e-e70e-4a4e-b41a-2a1453462a88.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>Feedback - subgrid wskazujący na oceny powiązane z wydarzeniem. Formularz zawiera funkcję “Add New” umożliwiającą na szybkie stworzenie nowego rekordu oceny dla wybranego wydarzenia.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756229804321/0dc4a686-27cc-4578-89a2-7094179c2fed.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<p>Logika widoczności danych pól i sekcji w formularzu została zaimplementowana w skrypcie eventForm.js który został dodany jako <strong>Web resource</strong>.</p>
<p>Biorąc pod uwagę, że sekcja <strong>Metrics</strong> zawiera jedynie pola obliczeniowe, nie jest ona widoczna w momencie tworzenia nowego rekordu. Logika ta została zaimplementowana w funkcji setMetricsSectionVisibility(), gdzie sprawdzany jest typ formularza (wartość 1 wskazuje na formularz tworzenia rekordu) i na jego podstawie ustawiana jest widoczność sekcji. Funkcja ta została podpięta do zdarzenia OnLoad formularza.</p>
<pre><code class="lang-plaintext">function setMetricsSectionVisibility(executionContext) {
  const formContext = executionContext.getFormContext();
  const formType = formContext.ui.getFormType();

  const metricsSection = formContext.ui.tabs
    .get("General")
    .sections.get("metrics_section");

  if (metricsSection) {
    if (formType === 1) {
      metricsSection.setVisible(false);
    } else {
      metricsSection.setVisible(true);
    }
  }
}
</code></pre>
<p>Dodatkowo dodana została funkcja setAvaliableSeatsVisibility(), odpowiadająca za widoczność pola <strong>Available Seats</strong>. Sprawdzany jest typ formularza (czy jest to formularz edycji rekordu lub read-only) a następnie status wydarzenia. Biorąc pod uwagę nieistotność wartości odpowiadającej za dostępność miejsc dla wydarzenia które jest już zakończone, wartość ta jest ukrywana gdy status wydarzenia jest “Completed”.</p>
<pre><code class="lang-plaintext">function setAvaliableSeatsVisibility(executionContext) {
  const formContext = executionContext.getFormContext();
  const formType = formContext.ui.getFormType();

  if (formType === 2 || formType === 3) {
    const availableSeats = formContext.getControl("kr_availableseats");
    const eventStatus = formContext.getAttribute("kr_eventstatus").getValue();

    if (availableSeats &amp;&amp; eventStatus) {
      if (eventStatus !== 124070004) {
        availableSeats.setVisible(true);
      } else {
        availableSeats.setVisible(false);
      }
    }
  }
}
</code></pre>
<p>Kolejnym polem obliczeniowym sekcji Metrics, którego widoczność została uwarunkowana w skrypcie, jest pole <strong>AverageRating</strong>. Biorąc pod uwagę, że użytkownik nie może dodać oceny do wydarzenia którego status jest inny niż “Completed“ pole to jest ukrywane do momentu aż wydarzenie osiągnie wspomniany status.</p>
<pre><code class="lang-plaintext">function setAverageRatingVisibility(executionContext) {
  const formContext = executionContext.getFormContext();
  const formType = formContext.ui.getFormType();

  if (formType === 2 || formType === 3) {
    const averageRating = formContext.getControl("kr_averagerating");
    const eventStatus = formContext.getAttribute("kr_eventstatus").getValue();

    if (averageRating &amp;&amp; eventStatus) {
      if (eventStatus === 124070004) {
        averageRating.setVisible(true);
      } else {
        averageRating.setVisible(false);
      }
    }
  }
}
</code></pre>
<p>Jeżeli chodzi o widoczność sekcji <strong>Participants</strong>, zastosowano bliźniaczą logikę jak w przypadku sekcji Metrics.</p>
<pre><code class="lang-plaintext">function setparticipantsSectionVisibility(executionContext) {
  const formContext = executionContext.getFormContext();
  const formType = formContext.ui.getFormType();

  const participantsSection = formContext.ui.tabs
    .get("General")
    .sections.get("participants_section");

  if (participantsSection) {
    if (formType === 1) {
      participantsSection.setVisible(false);
    } else {
      participantsSection.setVisible(true);
    }
  }
}
</code></pre>
<p>Nieco inne podejście zastosowano przy widoczności sekcji <strong>Feedback</strong>. Sprawdzany jest typ formularza (czy jest to formularz edycji rekordu lub read-only) oraz status wydarzenia. Sekcja ta jest widoczna jedynie przy statusie wydarzenia “Completed“ - biorąc pod uwagę fakt, że użytkownik nie może dodawać oceny do wydarzenia z innym statusem.</p>
<pre><code class="lang-plaintext">function setFeedbackSectionVisibility(executionContext) {
  const formContext = executionContext.getFormContext();
  const formType = formContext.ui.getFormType();

  const feedbackSection = formContext.ui.tabs
    .get("General")
    .sections.get("feedback_section");

  if (formType === 2 || formType === 3) {
    const eventStatus = formContext.getAttribute("kr_eventstatus").getValue();
    if (eventStatus === 124070004) {
      feedbackSection.setVisible(true);
    } else {
      feedbackSection.setVisible(false);
    }
  } else {
    feedbackSection.setVisible(false);
  }
}
</code></pre>
<h3 id="heading-42-formularz-tabeli-participants">4.2. Formularz Tabeli Participants</h3>
<p>Formularz podzielono na 2 sekcje:</p>
<ul>
<li><p>Participant Details - zawiera podstawowe dane osobowe uczestnika oraz status uczestnictwa: First Name, Last Name, Email, Phone, Registration Status</p>
</li>
<li><p>Event Info - zawiera informacje o wydarzeniu w którym użytkownik chce uczestniczyć: Event</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756231948730/a6a30a03-1f2a-46e7-9a2b-5e48692f5c15.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h3 id="heading-43-formularz-tabeli-feedback">4.3. Formularz Tabeli Feedback</h3>
<p>Formularz podzielono na 2 sekcje:</p>
<ul>
<li><p>Feedback Details - zawiera ocenę wydarzenia oraz opcjonalny komentarz: Rating, Comment</p>
</li>
<li><p>Event Info - zawiera informacje o wydarzeniu którego dotyczy ocena: Event</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756232231390/040d4483-3c27-4828-a369-fb7131ca6c3e.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<h2 id="heading-5-walidacja-formularzy">5. Walidacja formularzy</h2>
<p>Walidacja formularzy w aplikacjach model-driven jest kluczowa, aby zapewnić spójność i poprawność danych wprowadzanych przez użytkowników. W projekcie EventFlow logika walidacyjna została zrealizowana głównie z wykorzystaniem skryptów <strong>JavaScript</strong>, które pozwalają na implementację niestandardowych reguł biznesowych. Dodatkowo, w celu zaprezentowania możliwości konfiguracji wizualnej, w aplikacji została zaimplementowana jedna reguła biznesowa <strong>(Business Rule)</strong>.</p>
<h3 id="heading-51-walidacja-formularza-tabeli-event">5.1. Walidacja formularza tabeli Event</h3>
<p>Walidacja w formularzu wydarzeń polega głównie na walidacji daty. Należało pamiętać o kilku zasadach. Między innymi o uniemożliwieniu użytkownikowi wprowadzenia daty zakończenia wydarzenia późniejszej niż daty rozpoczęcia. Walidacja ta została zaimplementowana za pomocą Business Rule w tabeli Event.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756292313035/abad1d81-950e-4990-b9e6-213c8e9b8e98.png" alt class="image--center mx-auto" /></p>
<p>Kolejną zasadą jest uniemożliwienie użytkownikowi dodania wydarzenia z datą wsteczną. Walidacja ta została zaimplementowana w funkcji validateEventDate() w skrypcie eventForm.js. Funkcja ta została podpięta do zdarzenia OnChange wartości Start Date.</p>
<pre><code class="lang-plaintext">function validateEventDate(executionContext) {
  const formContext = executionContext.getFormContext();

  const startDateValue = formContext.getAttribute("kr_startdate").getValue();
  const startDateCtrl = formContext.getControl("kr_startdate");


  if(startDateValue &amp;&amp;startDateCtrl ) {
    const now = new Date();

    if(startDateValue &lt; now) {
      startDateCtrl.setNotification("Start Date cannot be in the past.", "startDateError");
    } else {
      startDateCtrl.clearNotification("startDateError");
    }
  }
}
</code></pre>
<p>Dodatkowo zaimplementowana została funkcja setEndDateSugestion() która ustawia datę zakończenia wydarzenia na datę początkową, po wybraniu wartości Star Date. Z reguły wydarzenia kończą się tego samego dnia, dzięki temu wybór zakończenia wydarzenia najprawdopodobniej ograniczy się do ustawiania odpowiedniej godziny zakończenia. Funkcja ta została podpięta do zdarzenia OnChange wartości StartDate.</p>
<pre><code class="lang-plaintext">function setEndDateSuggestion(executionContext) {
  const formContext = executionContext.getFormContext();

  const startDate = formContext.getAttribute("kr_startdate").getValue();
  const endDate = formContext.getAttribute("kr_enddate");

  if (startDate &amp;&amp; endDate) {
    endDate.setValue(startDate);
    endDate.setSubmitMode("always");
  }
}
</code></pre>
<h3 id="heading-52-walidacja-formularza-tabeli-participant">5.2. Walidacja formularza tabeli Participant</h3>
<p>Walidacja formularza tabeli Participant została zaimplementowana w skrypcie participantForm.js który został dodany jako jeden z Web resource projektu.</p>
<p>Funkcja validatePhoneNumber() sprawdza czy wprowadzony numer telefonu użytkownika ma poprawny format (zgodny z zasadami polskich numerów telefonów). Funkcja ta została dodana do zdarzenia OnChange pola Phone oraz OnSave formularza.</p>
<pre><code class="lang-plaintext">function validatePhoneNumber(executionContext) {
  const formContext = executionContext.getFormContext();
  const phoneAttr = formContext.getAttribute("kr_phone");
  const ctrl = formContext.getControl("kr_phone");

  if (!phoneAttr || !ctrl) return;

  const phoneValue = phoneAttr.getValue();

  const phoneRegex = /^(?:\+48|0048)?[\s-]?\d{3}[\s-]?\d{3}[\s-]?\d{3}$/;

  if (phoneValue &amp;&amp; !phoneRegex.test(phoneValue)) {
    ctrl.setNotification("Wrong phone number.", "phoneError");
  } else {
    ctrl.clearNotification("phoneError");
  }
}
</code></pre>
<p>Oprócz tego, w skrypcie zaimplementowano funkcje setLookupFilter() oraz validateEventOnSave().</p>
<p>Funkcja setLookupFilter() ogranicza listę wydarzeń widocznych w polu Event tylko do tych, które mają status “Registration” oraz posiadają wolne miejsca (Available Seats ≠ 0). Funkcja ta jest podłączona do zdarzenia OnLoad formularza.</p>
<pre><code class="lang-plaintext">function setEventLookupFilter(executionContext) {
  const formContext = executionContext.getFormContext();

  const eventLookupAttr = formContext.getAttribute("kr_event");
  const eventLookupCtrl = formContext.getControl("kr_event");

  if (eventLookupAttr &amp;&amp; eventLookupCtrl) {
    const registrationEventStatus = 124070001;

    eventLookupCtrl.addPreSearch(function () {
      const filter = `
                &lt;filter type="and"&gt;
                    &lt;condition attribute="kr_eventstatus" operator="eq" value="${registrationEventStatus}" /&gt;
                    &lt;condition attribute="kr_availableseats" operator="neq" value="0" /&gt;
                &lt;/filter&gt;
            `;

      eventLookupCtrl.addCustomFilter(filter);
    });
  }
}
</code></pre>
<p>Z kolei validateEventOnSave() działa podczas zapisywania formularza (zdarzenie OnSave) uczestnika i sprawdza, czy wybrane wydarzenie spełnia warunki rejestracji. Weryfikuje ona, czy status wydarzenia to “Registration” oraz czy istnieją wolne miejsca. Jeśli którykolwiek z warunków nie jest spełniony, zapis zostaje zablokowany, a użytkownik otrzymuje komunikat ostrzegawczy. Ze względu na ograniczenia planu Developer, który nie obsługuje wywołań asynchronicznych, implementacja musiała zostać wykonana w trybie synchronicznym.</p>
<p>Funkcja ta została zaimplementowana, ponieważ użytkownik ma możliwość dodania uczestnika bezpośrednio z poziomu widoku wydarzenia. W takiej sytuacji istnieje ryzyko, że uczestnik zostanie przypisany do wydarzenia niespełniającego założonych warunków (np. o statusie innym niż “Registration” lub z brakiem dostępnych miejsc). Funkcja ta pełni rolę mechanizmu zabezpieczającego – wymusza ponowną weryfikację stanu wydarzenia w momencie zapisu i zapobiega wprowadzeniu niepoprawnych danych do systemu.</p>
<pre><code class="lang-plaintext">function validateEventOnSave(executionContext) {
  const formContext = executionContext.getFormContext();
  const selectedEvent = formContext.getAttribute("kr_event").getValue();

  if (!selectedEvent) return;

  const eventId = selectedEvent[0].id.replace(/[{}]/g, "");
  const req = new XMLHttpRequest();
  req.open("GET", Xrm.Utility.getGlobalContext().getClientUrl() + 
    `/api/data/v9.2/kr_events(${eventId})?$select=kr_eventstatus,kr_availableseats`, false);
  req.setRequestHeader("OData-MaxVersion", "4.0");
  req.setRequestHeader("OData-Version", "4.0");
  req.setRequestHeader("Accept", "application/json");
  req.setRequestHeader("Content-Type", "application/json; charset=utf-8");

  req.send();

  if (req.status === 200) {
    const eventRecord = JSON.parse(req.responseText);
    const eventStatus = eventRecord["kr_eventstatus"];
    const availableSeats = eventRecord["kr_availableseats"];

    if (eventStatus !== 124070001 || availableSeats === 0) {
      executionContext.getEventArgs().preventDefault();
      Xrm.Navigation.openAlertDialog({
        text: "Cannot register - the event is not in Registration status or there are no available seats."
      });
    }
  } else {
    console.error("Error fetching event:", req.statusText);
    executionContext.getEventArgs().preventDefault();
  }
}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756294919858/b1bc03a0-c193-4796-8486-ab6cc9efe20b.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-53-walidacja-formularza-tabeli-feedback">5.3. Walidacja formularza tabeli Feedback</h3>
<p>Walidacja formularza tabeli Feedback została zaimplementowana w skrypcie feedbackForm.js, dodanym jako zasób typu Web Resource. Podobnie jak w przypadku formularza Participants, konieczne było ograniczenie listy dostępnych wydarzeń w polu Event tak, aby użytkownik mógł powiązać ocenę wyłącznie z poprawnym rekordem. Zgodnie z założeniami, oceny mogą być dodawane tylko do wydarzeń o statusie „Completed”.</p>
<p>Logika ta została zaimplementowana w funkcji setEventLookupFilter(), wywoływanej podczas zdarzenia OnLoad formularza. Funkcja dynamicznie filtruje zawartość pola lookup i udostępnia użytkownikowi jedynie wydarzenia zakończone.</p>
<pre><code class="lang-plaintext">function setEventLookupFilter(executionContext) {
  const formContext = executionContext.getFormContext();

  const eventLookupAttr = formContext.getAttribute("kr_event");
  const eventLookupCtrl = formContext.getControl("kr_event");

  if (eventLookupAttr &amp;&amp; eventLookupCtrl) {
    const completeEventStatus = 124070004;

    eventLookupCtrl.addPreSearch(function () {
      const filter = `
                &lt;filter type="and"&gt;
                    &lt;condition attribute="kr_eventstatus" operator="eq" value="${completeEventStatus}" /&gt;
                &lt;/filter&gt;
            `;

      eventLookupCtrl.addCustomFilter(filter);
    });
  }
}
</code></pre>
<p>Warto zauważyć, że w tym przypadku nie ma konieczności dodawania dodatkowych mechanizmów zabezpieczających (tak jak w tabeli Participants). Wynika to z faktu, iż subgrid tabeli Feedback na formularzu Event jest widoczny wyłącznie wtedy, gdy wydarzenie posiada status „Completed”. Tym samym sama logika aplikacji uniemożliwia dodanie niepoprawnego wpisu.</p>
<h2 id="heading-6-tworzenie-wartosci-event-indicator-primary-name-column-dla-tabeli-event">6. Tworzenie wartości Event Indicator (Primary Name Column dla tabeli Event)</h2>
<p>W każdej tabeli w Dataverse istnieje jedno pole oznaczone jako Primary Name Column. Jest to kolumna główna, której wartości służą m.in do identyfikowania rekordów w interfejsie użytkownika (np. w polach typu lookup).</p>
<p>W tabeli Event funkcję tę pełni pole Event Indicator. Funkcja setEventIndicator() została stworzona w celu automatycznego generowania spójnej wartości dla tego pola, bez konieczności ingerencji użytkownika. Dzięki temu wartość jest łatwa do zidentyfikowania podczas dodawania oceny lub rejestracji uczestnika na wydarzenie. Sama nazwa wydarzenia może być niewystarczająca, ponieważ istnieje prawdopodobieństwo, że w dłuższej perspektywie czasu powstaną dwa wydarzenia o takiej samej nazwie (np. cykliczne szkolenia lub warsztaty).</p>
<p>Z tego powodu pole Event Indicator budowane jest w oparciu o dwie wartości: Event Name oraz Start Date. Dzięki temu każdemu rekordowi przypisany zostaje jednoznaczny identyfikator (np. <em>Warsztaty CRM 15-03-2025</em>), co ułatwia rozróżnianie poszczególnych wydarzeń.</p>
<pre><code class="lang-plaintext">function setEventIndicator(executionContext) {
  const formContext = executionContext.getFormContext();

  const eventName = formContext.getAttribute("kr_eventname").getValue();
  const startDate = formContext.getAttribute("kr_startdate").getValue();

  if (eventName &amp;&amp; startDate) {
    const day = String(startDate.getDate()).padStart(2, "0");
    const month = String(startDate.getMonth() + 1).padStart(2, "0");
    const year = startDate.getFullYear();

    formContext
      .getAttribute("kr_eventnumber")
      .setValue(eventName + " " + `${day}-${month}-${year}`);
    formContext.getAttribute("kr_eventnumber").setSubmitMode("always");
  }
}
</code></pre>
<p>Funkcja została powiązana ze zdarzeniem OnSave formularza, dzięki czemu wartość pola Event Indicator generowana jest automatycznie w momencie zapisywania rekordu do bazy danych.</p>
<h2 id="heading-7-zarzadzanie-statusami-wydarzenia">7. Zarządzanie statusami wydarzenia</h2>
<p>Azure Functions to środowisko uruchomieniowe w chmurze Microsoftu, które pozwala uruchamiać małe fragmenty kodu w odpowiedzi na różne zdarzenia, takie jak HTTP requesty, zdarzenia w kolejce, zmiany w bazie danych czy wyzwalacze czasowe.</p>
<p>W projekcie przewidziano funkcję automatycznego ustawiania statusów wydarzeń, która ułatwia monitorowanie cyklu życia każdego eventu. Na potrzeby naszego projektu została zaimplementowana funkcja Azure UpdateEventTableStatuses.</p>
<pre><code class="lang-plaintext">const tokenEndpoint = (tenantId) =&gt;
  `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;

async function getAccessToken({ tenantId, clientId, clientSecret, dataverseUrl }) {
  const body = new URLSearchParams({
    client_id: clientId,
    client_secret: clientSecret,
    scope: `${dataverseUrl}/.default`,
    grant_type: "client_credentials",
  });

  const res = await fetch(tokenEndpoint(tenantId), {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body,
  });

  if (!res.ok) throw new Error(await res.text());
  const data = await res.json();
  return data.access_token;
}

async function getRecords({ dataverseUrl, table, token }) {
  const res = await fetch(`${dataverseUrl}/api/data/v9.2/${table}?$select=kr_startdate,kr_enddate,kr_eventstatus,kr_availableseats`, {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: "application/json",
    },
  });
  if (!res.ok) throw new Error(await res.text());
  const data = await res.json();
  return data.value;
}

async function updateRecord({ dataverseUrl, table, id, body, token }) {
  const res = await fetch(`${dataverseUrl}/api/data/v9.2/${table}(${id})`, {
    method: "PATCH",
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
      Accept: "application/json",
      "If-Match": "*",
    },
    body: JSON.stringify(body),
  });
  if (!res.ok) throw new Error(await res.text());
}

function computeEventStatus(record) {
  const statuses = {
    registration: 124070001,
    full: 124070005,
    inProgress: 124070003,
    complete: 124070004,
  };

  const startDate = record.kr_startdate ? new Date(record.kr_startdate) : null;
  const endDate = record.kr_enddate ? new Date(record.kr_enddate) : null;
  const currentStatus = record.kr_eventstatus;
  const availableSeats = record.kr_availableseats;
  const now = new Date();

  if (!startDate || !endDate) return null;

  if (currentStatus === statuses.registration &amp;&amp; availableSeats === 0) {
    return statuses.full;
  } else if (startDate &lt;= now &amp;&amp; endDate &gt;= now) {
    return statuses.inProgress;
  } else if (endDate &lt; now) {
    return statuses.complete;
  }
  return null;
}

module.exports = async function (context, myTimer) {
  const tenantId = process.env.TENANT_ID;
  const clientId = process.env.CLIENT_ID;
  const clientSecret = process.env.CLIENT_SECRET;
  const dataverseUrl = process.env.DATAVERSE_URL;
  const table = process.env.TABLE_LOGICAL_NAME;
  const pk = process.env.PRIMARY_KEY_LOGICAL_NAME;

  const token = await getAccessToken({ tenantId, clientId, clientSecret, dataverseUrl });
  const records = await getRecords({ dataverseUrl, table, token });

  for (let r of records) {
    cosnt newStatus = computeEventStatus(r);
    if (newStatus &amp;&amp; newStatus !== r.kr_eventstatus) {
      await updateRecord({ dataverseUrl, table, id: r[pk], body: { kr_eventstatus: newStatus }, token });
    }
  }
};
</code></pre>
<p>Jak działa nasze rozwiązanie:</p>
<ol>
<li><p><strong>Pobranie danych uwierzytelniających</strong><br /> Funkcja najpierw odczytuje z ustawień aplikacji wartości niezbędne do połączenia z Dataverse: tenant ID, client ID i client secret, URL środowiska Dataverse, nazwę tabeli oraz jej klucz główny.</p>
</li>
<li><p><strong>Pozyskanie tokenu dostępu</strong><br /> Na podstawie tych danych funkcja wykonuje żądanie do Azure AD w celu uzyskania tokenu OAuth2. Token pozwala funkcji na autoryzowany dostęp do Dataverse i wykonywanie operacji na danych bez udziału użytkownika.</p>
</li>
<li><p><strong>Pobranie rekordów z tabeli wydarzeń</strong><br /> Po uzyskaniu tokenu funkcja pobiera wszystkie rekordy z tabeli wydarzeń, wybierając tylko kluczowe pola: daty rozpoczęcia i zakończenia, aktualny status wydarzenia oraz liczbę dostępnych miejsc.</p>
</li>
<li><p><strong>Tworzenie nowego statusu wydarzenia</strong><br /> Dla każdego rekordu funkcja analizuje jego dane i decyduje o odpowiednim statusie:</p>
<ul>
<li><p>jeśli wydarzenie jest w fazie rejestracji i nie ma już wolnych miejsc - status zmienia się na „Full”,</p>
</li>
<li><p>jeśli obecna pora jest pomiędzy rozpoczęciem a zakończeniem wydarzenia- status zmienia się na „In Progress”,</p>
</li>
<li><p>jeśli obecna pora jest późniejsza niż zakończenie wydarzenia - status zmienia się na „Completed”.</p>
</li>
</ul>
</li>
<li><p><strong>Aktualizacja rekordów w Dataverse</strong><br /> Jeśli nowy status różni się od obecnego, funkcja wysyła żądanie aktualizacji do Dataverse, zmieniając status wydarzenia w tabeli.</p>
</li>
<li><p><strong>Automatyczne wywołanie co określony czas</strong><br /> Funkcja jest wyzwalana przez mechanizm Timer Trigger w Azure Functions. Dzięki temu proces aktualizacji statusów odbywa się automatycznie co 30 minut, bez potrzeby ręcznego uruchamiania lub monitorowania.</p>
</li>
<li><p><strong>Bezpieczeństwo i uprawnienia</strong><br /> Funkcja korzysta z Application User w Dataverse, któremu przypisano rolę Organizer umożliwiającą na odczyt i manipulację rekordów w tabeli Event. Dzięki temu operacje wykonywane przez funkcję są w pełni bezpieczne i zgodne z zasadami uprawnień w Dataverse.</p>
</li>
</ol>
<p>W naszym projekcie Azure Function okazała się idealnym rozwiązaniem do automatycznego ustawiania statusów wydarzeń w Dataverse. Funkcjonalność ta pozwoliła nam obejść ograniczenia konta developer, które nie daje dostępu do premium connectorów w Power Automate.</p>
<h2 id="heading-8-wykresy-i-dashboardy">8. Wykresy i dashboardy</h2>
<p>W aplikacjach model-driven wykresy i dashboardy pełnią kluczową rolę w prezentacji danych w sposób wizualny i przystępny. Dzięki nim użytkownicy mogą szybko analizować informacje, wyciągać wnioski i podejmować decyzje bez konieczności przeglądania długich list rekordów.</p>
<ul>
<li><p>Wykresy (Charts) – to wizualizacje oparte na danych z tabel, które pozwalają przedstawić trendy, porównania czy rankingi. Mogą być wyświetlane samodzielnie lub jako część formularza czy dashboardu.</p>
</li>
<li><p>Dashboardy – to zbiory widoków, wykresów i list, które w jednym miejscu dostarczają użytkownikowi całościowego obrazu sytuacji biznesowej. Dzięki nim możliwe jest monitorowanie kluczowych wskaźników w czasie rzeczywistym.</p>
</li>
</ul>
<p>W projekcie EventFlow przygotowano główny dashboard, którego celem jest dostarczenie organizatorom i uczestnikom szybkiego wglądu w najważniejsze informacje o wydarzeniach:</p>
<ul>
<li><p>Wykres 1 – Top Rated Events - Ranking pięciu najlepiej ocenianych wydarzeń. Dzięki niemu można szybko sprawdzić, które szkolenia czy konferencje były najlepiej odbierane przez uczestników.</p>
</li>
<li><p>Widok Upcoming Events - Lista wszystkich zaplanowanych wydarzeń, które jeszcze się nie rozpoczęły. Widok ten umożliwia szybkie sprawdzenie harmonogramu i dostępności miejsc.</p>
</li>
<li><p>Wykres 2 – Attendance - Ranking pięciu wydarzeń z największą frekwencją. Wykres ten pozwala ocenić, które wydarzenia cieszyły się największym zainteresowaniem.</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756287577909/5129123d-7703-4c41-9618-016ac1ee0333.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-9-business-process-flow">9. Business Process Flow</h2>
<p>Business Process Flow w aplikacjach model-driven to mechanizm pozwalający definiować i wizualizować kolejne etapy procesu biznesowego, które użytkownik powinien wykonać podczas pracy z rekordem. BPF prowadzi użytkownika krok po kroku, podpowiadając wymagane pola i działania na każdym etapie, a także zapewnia spójność danych i ułatwia ich kompletność.</p>
<p>W aplikacji EventFlow zdefiniowano Business Process Flow o nazwie Event Lifecycle Process, który odzwierciedla cykl życia wydarzenia i prowadzi użytkownika przez kolejne etapy.</p>
<ol>
<li><p>Planning – pierwszy etap, w którym użytkownik wprowadza podstawowe informacje o wydarzeniu. Wymagane pola to: Event Name, Start Date, End Date, Capacity oraz Organizer.</p>
</li>
<li><p>Event Preparation – etap przygotowania wydarzenia, w którym należy określić status wydarzenia.</p>
</li>
<li><p>Post-Event Evaluation – etap końcowy, odpowiadający zakończeniu wydarzenia i możliwości jego oceny. Do jego zamknięcia konieczne jest aby rekord posiadał wartości w polach: Average Rating oraz Number Of Responses, co umożliwia pełną ocenę i analizę wydarzenia.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756236267272/176050e8-c102-4f9d-82eb-28d25d353747.png" alt class="image--center mx-auto" /></p>
<p>Event Lifecycle Process wspiera użytkowników w uporządkowany sposób, od planowania po ocenę wydarzenia, odzwierciedlając rzeczywiste etapy cyklu życia wydarzenia.</p>
<h2 id="heading-10-site-map">10. Site Map</h2>
<p>W aplikacjach model-driven nawigacja opiera się na tzw. <strong>Site Map</strong> – jest to mapa witryny, która określa, jakie obszary i grupy funkcjonalne widzi użytkownik w menu aplikacji. Site Map pozwala uporządkować tabele, formularze, dashboardy i raporty w logiczne sekcje, ułatwiając dostęp do danych i funkcji.</p>
<p>W projekcie EventFlow nawigacja została podzielona na dwa główne obszary:</p>
<ul>
<li><p>Event Management → Insights<br />  Udostępnia dashboard Events Overview, który wizualizuje dane o wydarzeniach, uczestnikach i ocenach, wspierając analizę i monitorowanie wydarzeń.</p>
</li>
<li><p>Event Management → Operations<br />  Zawiera dostęp do głównych tabel: Event, Participant i Feedback, umożliwiając zarządzanie wydarzeniami, rejestrację uczestników oraz przegląd anonimowych ocen.</p>
</li>
</ul>
<p>Takie rozdzielenie funkcji pozwala użytkownikom szybko odnaleźć potrzebne informacje i wykonywać zadania zgodnie z ich rolą w firmie.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756235163170/762f2dac-793b-4e32-b401-50e44015718a.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756235320465/8fbe74fc-206c-4912-b56f-eb4c36820820.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-11-uprawnienia-uzytkownikow">11. Uprawnienia użytkowników</h2>
<p>W aplikacji EventFlow wyróżniono dwie główne role systemowe:</p>
<ul>
<li><p>Participant / Evaluator – rola przeznaczona dla pracowników biorących udział w wydarzeniach i wystawiających oceny.</p>
</li>
<li><p>Organizer – rola dla osób odpowiedzialnych za organizację wydarzeń i zarządzanie uczestnikami.</p>
</li>
</ul>
<p>Podział ról został zaprojektowany w taki sposób, aby każdy użytkownik posiadał wyłącznie niezbędne uprawnienia, a jednocześnie dane w systemie były w pełni bezpieczne.</p>
<h3 id="heading-111-rola-participant-evaluator">11.1. Rola Participant / Evaluator</h3>
<p>Rola ta skupia się na użytkownikach końcowych, którzy korzystają z aplikacji w celu uczestniczenia w wydarzeniach oraz oceniania ich po zakończeniu.</p>
<ul>
<li><p><strong>Tabela Event:</strong></p>
<ul>
<li><p>Read (Organization) – dostęp tylko do odczytu wszystkich wydarzeń.</p>
</li>
<li><p>Brak możliwości tworzenia, edycji czy usuwania wydarzeń.</p>
</li>
<li><p>Append/Append To (Organization) – na potrzeby relacji lookup.</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Participant:</strong></p>
<ul>
<li><p>Create (User) – użytkownik może zapisać się na wydarzenie.</p>
</li>
<li><p>Read (Organization) – dostęp do listy uczestników wydarzenia.</p>
</li>
<li><p>Write (User) – możliwość edycji własnej rejestracji (np. zmiana statusu na “Cancelled”).</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Feedback:</strong></p>
<ul>
<li><p>Create (User) – możliwość dodania oceny i komentarza do wydarzenia.</p>
</li>
<li><p>Read (Organization) – możliwość przeglądania ocen powiązanych z wydarzeniem.</p>
</li>
<li><p>Write (User) – edycja własnych ocen.</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Contact:</strong></p>
<ul>
<li><p>Read (Organization) – wgląd w podstawowe dane kontaktowe organizatora (Lookup).</p>
</li>
<li><p>Append/Append To (Organization) – na potrzeby relacji lookup.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-112-rola-organizer">11.2. Rola Organizer</h3>
<p>Rola ta została zaprojektowana dla osób zarządzających wydarzeniami. Obejmuje szersze uprawnienia, które umożliwiają pełną kontrolę nad procesem planowania, organizacji i ewaluacji.</p>
<ul>
<li><p><strong>Tabela Event:</strong></p>
<ul>
<li><p>Create (Organization) – możliwość tworzenia nowych wydarzeń.</p>
</li>
<li><p>Read (Organization) – pełny wgląd we wszystkie wydarzenia.</p>
</li>
<li><p>Write (Organization) – edycja wszystkich wydarzeń.</p>
</li>
<li><p>Delete (Organization) – możliwość usuwania wszystkich wydarzeń.</p>
</li>
<li><p>Append/Append To (Organization) – na potrzeby relacji lookup.</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Participant:</strong></p>
<ul>
<li><p>Create (Organization) – możliwość dodania uczestnika do wydarzenia.</p>
</li>
<li><p>Read (Organization) – pełny dostęp do listy uczestników.</p>
</li>
<li><p>Write (Organization) – edycja dowolnych rekordów uczestników (np. potwierdzanie uczestnictwa).</p>
</li>
<li><p>Delete (Organization) – możliwość usuwania wszystkich rekordów uczestników.</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Feedback:</strong></p>
<ul>
<li><p>Read (Organization) – dostęp do wszystkich ocen.</p>
</li>
<li><p>Brak możliwości usuwania i edycji rekordów.</p>
</li>
</ul>
</li>
<li><p><strong>Tabela Contact:</strong></p>
<ul>
<li><p>Read (Organization) – dostęp do pełnej listy kontaktów.</p>
</li>
<li><p>Append/Append To (Organization) – na potrzeby relacji lookup.</p>
</li>
</ul>
</li>
</ul>
<h2 id="heading-12-wnioski-i-podsumowanie">12. Wnioski i podsumowanie</h2>
<p>Projekt EventFlow pokazał, że aplikacje model-driven w Microsoft Power Platform pozwalają w szybki i prosty sposób tworzyć kompleksowe rozwiązania biznesowe oparte na danych z Dataverse.</p>
<p>W trakcie realizacji projektu udało się między innymi:</p>
<ul>
<li><p>Zaprojektować struktury danych aplikacji.</p>
</li>
<li><p>Stworzyć formularze z logiką walidacji i regułami widoczności pól oraz sekcji.</p>
</li>
<li><p>Zaimplementować automatyzację statusów wydarzeń w oparciu o daty i liczbę dostępnych miejsc.</p>
</li>
<li><p>Skonfigurować role i uprawnienia użytkowników.</p>
</li>
<li><p>Utworzyć dashboardy, wykresy i widoki umożliwiające szybki wgląd w najważniejsze wskaźniki wydarzeń.</p>
</li>
</ul>
<p>Podsumowując, EventFlow stanowi przykład profesjonalnej aplikacji biznesowej typu model-driven, która łączy intuicyjny interfejs z wydajną logiką biznesową i umożliwia kompleksowe zarządzanie wydarzeniami w firmie. Pomimo pewnych ograniczeń środowiska deweloperskiego, aplikacja w pełni spełnia założenia projektowe i może być w prosty sposób rozszerzana w przyszłości.</p>
]]></content:encoded></item><item><title><![CDATA[Aplikacja rezerwacji sal konferencyjnych w Power Apps - od pomysłu do działającej aplikacji]]></title><description><![CDATA[Rezerwacja sali konferencyjnej to jedno z zadań, z którym możemy spotkać się w codziennej pracy biurowej. W tym artykule pokażę, jak zbudowałam w Power Apps aplikację Canvas App Sala+, umożliwiającą rezerwację sal konferencyjnych – od zaprojektowania...]]></description><link>https://karolinaruda.hashnode.dev/aplikacja-rezerwacji-sal-konferencyjnych-w-power-apps-od-pomyslu-do-dzialajacej-aplikacji</link><guid isPermaLink="true">https://karolinaruda.hashnode.dev/aplikacja-rezerwacji-sal-konferencyjnych-w-power-apps-od-pomyslu-do-dzialajacej-aplikacji</guid><category><![CDATA[PowerPlatform]]></category><category><![CDATA[powerapps]]></category><category><![CDATA[Canvas PowerApps]]></category><category><![CDATA[#canvasapps]]></category><category><![CDATA[Dataverse]]></category><category><![CDATA[automation]]></category><dc:creator><![CDATA[Karolina Ruda]]></dc:creator><pubDate>Wed, 20 Aug 2025 08:06:28 GMT</pubDate><content:encoded><![CDATA[<p>Rezerwacja sali konferencyjnej to jedno z zadań, z którym możemy spotkać się w codziennej pracy biurowej. W tym artykule pokażę, jak zbudowałam w Power Apps aplikację Canvas App <strong>Sala+</strong>, umożliwiającą rezerwację sal konferencyjnych – od zaprojektowania struktury danych w Dataverse, przez formularze i walidację rezerwacji, aż po automatyczną aktualizacje statusów i wysyłanie powiadomień.</p>
<h2 id="heading-1-zalozenia-projektu">1. Założenia projektu</h2>
<p>Celem projektu jest stworzenie profesjonalnej aplikacji biznesowej, która umożliwi pracownikom firmy łatwe i bezproblemowe rezerwowanie dostępnych sal konferencyjnych. Aplikacja powinna łączyć intuicyjny interfejs z wydajną logiką biznesową oraz zapewniać pełną kontrolę nad danymi w backendzie.</p>
<p>Użytkownicy aplikacji powinni mieć możliwość:</p>
<ul>
<li><p>Przeglądania listy dostępnych sal konferencyjnych wraz z ich opisem, lokalizacją, wyposażeniem i aktualnym statusem dostępności.</p>
</li>
<li><p>Korzystania z widoku kalendarza, który pokazuje zajęte i wolne terminy sal.</p>
</li>
<li><p>Dodawania rezerwacji poprzez formularz, z uwzględnieniem walidacji danych oraz sprawdzaniem kolizji terminów.</p>
</li>
<li><p>Przeglądania własnych rezerwacji oraz anulowania ich w razie potrzeby, z ograniczeniem: anulowanie rezerwacji jest niedozwolone na mniej niż 2 godziny przed rozpoczęciem spotkania.</p>
</li>
<li><p>(Dla użytkowników z uprawnieniami administratora) Zarządzania salami i rezerwacjami w panelu administracyjnym – dodawanie oraz edycja sal konferencyjnych oraz edycja rezerwacji.</p>
</li>
</ul>
<p>Podczas implementacji funkcjonalności należy uwzględnić:</p>
<ul>
<li><p>Walidację formularzy - tak aby wymagane pola były wypełnione, a wprowadzone dane poprawne.</p>
</li>
<li><p>Sprawdzanie kolizji terminów - aby jedna sala nie mogła być zarezerwowana w tym samym czasie przez różnych użytkowników.</p>
</li>
<li><p>Automatyczną aktualizację statusów rezerwacji po upływie terminu spotkania (np. zmiana statusu na „Zakończona”).</p>
</li>
<li><p>Bezpieczeństwo danych, poprzez przypisanie odpowiednich ról użytkownikom i ograniczenie widoczności oraz możliwości edycji danych w zależności od uprawnień.</p>
</li>
<li><p>Wsparcie dla automatycznych powiadomień i przypomnień o rezerwacjach</p>
</li>
</ul>
<p>Dzięki tym założeniom aplikacja będzie nie tylko praktyczna i funkcjonalna, ale również bezpieczna i łatwa w użyciu, spełniając potrzeby zarówno zwykłych użytkowników, jak i administratorów systemu.</p>
<h2 id="heading-2-tworzenie-tabel-i-relacji-w-dataverse">2. Tworzenie tabel i relacji w Dataverse</h2>
<p>Podstawą każdej aplikacji biznesowej jest dobrze zaprojektowany model danych. W tym projekcie wykorzystaliśmy Microsoft Dataverse do przechowywania informacji o salach konferencyjnych oraz rezerwacjach.</p>
<h3 id="heading-21-tabela-salekonferencyjne">2.1. Tabela SaleKonferencyjne</h3>
<p>Ta tabela przechowuje wszystkie informacje o dostępnych salach w firmie. Zawiera kolumny takie jak:</p>
<ul>
<li><p><strong>SalaID</strong> (Autonumber) - unikalny identyfikator sali</p>
</li>
<li><p><strong>NazwaSali</strong> (Text) - nazwa sali (wymagane)</p>
</li>
<li><p><strong>Lokalizacja</strong> (Text) - lokalizacja sali (wymagane)</p>
</li>
<li><p><strong>LiczbaMiejsc</strong> (Whole number) - liczba miejsc sali (wymagane)</p>
</li>
<li><p><strong>Opis</strong> (Text) - opis sali (wymagane)</p>
</li>
<li><p><strong>Dostępność</strong> (Yes/no) - informacja czy sala jest aktywna (wymagane)</p>
</li>
<li><p><strong>Wyposażenie</strong> (Multiselect choice) - np. projektor, TV, flipchart (opcjonalne)</p>
</li>
<li><p><strong>Zdjęcie</strong> (Image) - zdjęcie sali (opcjonalne)</p>
</li>
</ul>
<h3 id="heading-22-tabela-rezerwacjesal">2.2. Tabela RezerwacjeSal</h3>
<p>Tabela RezerwacjeSal odpowiada za przechowywanie wszystkich dokonanych rezerwacji. Zawiera kolumny:</p>
<ul>
<li><p><strong>RezerwacjaID</strong> (Autonumber) - unikalny identyfikator rezerwacji</p>
</li>
<li><p><strong>SalaID</strong> (Lookup do SaleKonferencyjne) - powiązanie z zarezerwowaną salą (wymagane)</p>
</li>
<li><p><strong>StartRezerwacji</strong> (Date and time) - początek rezerwacji (wymagane)</p>
</li>
<li><p><strong>KoniecRezerwacji</strong> (Date and time) - koniec rezerwacji (wymagane)</p>
</li>
<li><p><strong>StatusRezerwacji</strong> (Choice) - aktualny status rezerwacji: “Aktywna“, “Anulowana“ lub “Zakończona“ (wymagane)</p>
</li>
<li><p><strong>Cel</strong> (Text area) - cel spotkania (opcjonalne)</p>
</li>
</ul>
<p>W Dataverse każda tabela posiada systemową kolumnę <strong>CreatedBy</strong>, przechowującą informacje o użytkowniku, który utworzył dany rekord. W naszym projekcie kolumna ta niejednokrotnie będzie wykorzystywana m.in. do filtrowania rekordów oraz wyświetlania informacji o osobie, która dokonała konkretnej rezerwacji.</p>
<h3 id="heading-23-relacje-miedzy-tabelami">2.3. Relacje między tabelami</h3>
<p>W projekcie zastosowano relację jeden-do-wielu między tabelą SaleKonferencyjne a RezerwacjeSal. Oznacza to, że każda sala może mieć wiele rezerwacji, a każda rezerwacja jest przypisana tylko do jednej sali. Dzięki takiej strukturze łatwo filtrować rezerwacje dla konkretnej sali i utrzymać spójność danych w całym systemie.</p>
<h2 id="heading-3-projektowanie-ui-przeglad-ekranow-aplikacji">3. Projektowanie UI - przegląd ekranów aplikacji</h2>
<p>Aplikacja Sala+ została zaprojektowana z myślą o intuicyjnym i minimalistycznym interfejsie, który umożliwia użytkownikom łatwe przeglądanie dostępnych sal, zarządzanie rezerwacjami oraz kontrolowanie nadchodzących spotkań. Dodatkowo, dzięki zdefiniowaniu zmiennych przechowujących wartości kolorów w trigerze App OnStart, kolorystyka interfejsu może zostać w prosty sposób zmodyfikowana i dostosowana do aktualnych potrzeb.</p>
<p>Poniżej przedstawiam główne ekrany aplikacji wraz z opisem ich funkcjonalności i wizualnym podglądem.</p>
<h3 id="heading-31-ekran-startowy">3.1. Ekran startowy</h3>
<p>Ekran startowy pełni funkcję powitalną. Użytkownik widzi nazwę i logo aplikacji oraz spersonalizowane powitanie. Przycisk „Rozpocznij” pozwala przejść do głównych funkcjonalności aplikacji.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755536737830/0df7b01f-cb8e-45d3-8f8f-f803e9027306.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-32-ekran-sale-konferencyjne">3.2. Ekran “Sale Konferencyjne”</h3>
<p>Ten ekran umożliwia użytkownikom przeglądanie dostępnych sal konferencyjnych. Zawiera listę sal wraz z podstawowymi informacjami, takimi jak nazwa oraz opis.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755536897890/0df6b024-bfa4-4ad3-949d-165ece4b8709.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu w “Szczegóły“ użytkownik może zapoznać się z bardziej szczegółowymi informacjami na temat wybranej sali oraz przejść do formularza rezerwacji sal po kliknięciu przycisku “Zarezerwuj“.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755537265820/15557c5d-6417-4962-a955-c4fe7c5f75bb.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-33-ekran-kalendarz-rezerwacji">3.3. Ekran “Kalendarz rezerwacji“</h3>
<p>Ekran „Kalendarz rezerwacji” pozwala użytkownikowi łatwo przeglądać wszystkie dokonane rezerwacje w formie wizualnej. Kontrolka kalendarza wskazuje dni z rezerwacjami za pomocą kropek, a po kliknięciu w konkretną datę wyświetlana jest lista rezerwacji na ten dzień – w tym nazwa sali, godziny rozpoczęcia i zakończenia oraz osoba, która dokonała rezerwacji. Dodatkowo użytkownik może filtrować rezerwacje po nazwie sali lub szybko wyczyścić filtry, aby zobaczyć pełny harmonogram.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755538250745/45ed4fbb-28de-4126-a327-1d02147eb82a.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-34-ekran-dokonaj-rezerwacji">3.4. Ekran “Dokonaj rezerwacji“</h3>
<p>Ekran „Dokonaj rezerwacji” umożliwia użytkownikowi dodanie nowej rezerwacji poprzez intuicyjny formularz. Po prawej stronie znajduje się formularz, w którym użytkownik wybiera nazwę sali, datę i godzinę rozpoczęcia i zakończenia oraz cel spotkania. Po lewej stronie widoczny jest skrócony widok wybranej sali – nazwa, zdjęcie, liczba miejsc i wyposażenie – co pozwala użytkownikowi szybko zweryfikować szczegóły przed potwierdzeniem rezerwacji.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755538596468/a953f29d-76a4-4619-b883-fa0c783fbc8c.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-35-ekran-moje-rezerwacje">3.5. Ekran “Moje rezerwacje“</h3>
<p>Ekran „Moje rezerwacje” umożliwia użytkownikowi szybki podgląd wszystkich swoich rezerwacji. Na liście widoczna jest nazwa sali, godziny rozpoczęcia i zakończenia, status rezerwacji oraz jej cel. Jeśli rezerwacja ma status „Aktywna” i rozpoczęcie spotkania jest odległe o więcej niż 2 godziny, użytkownik widzi przycisk pozwalający na jej anulowanie. Dodatkowo możliwe jest filtrowanie rezerwacji po statusie, nazwie sali lub dacie oraz szybkie wyczyszczenie wszystkich filtrów, co ułatwia odnalezienie interesujących informacji.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755539078754/9e87ae97-3f81-4b50-b346-384be76837b4.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu przycisku “Anuluj rezerwację“ pojawia się popup z zapytaniem, czy użytkownik na pewno chce anulować rezerwację.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755539473770/a5ae0e7c-307b-4206-92af-a56bf077d342.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-36-ekran-panel-administratora-zakladka-rezerwacje">3.6. Ekran “Panel Administratora“ zakładka “Rezerwacje“</h3>
<p>Ekran „Panel administratora” – zakładka Rezerwacje umożliwia przeglądanie wszystkich rezerwacji dokonanych przez użytkowników. Na liście widoczna jest nazwa sali, godziny rozpoczęcia i zakończenia, status rezerwacji oraz użytkownik, który ją utworzył. Administrator ma możliwość edycji lub usunięcia każdej rezerwacji. Dodatkowo dostępne są filtry pozwalające wyszukiwać rezerwacje według nazwy użytkownika, statusu, nazwy sali oraz daty, a także opcja szybkiego wyczyszczenia wszystkich zastosowanych filtrów.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755539583716/4de244d0-f391-4dfd-a2b0-5ec5001c9068.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu przycisku “Edytuj“ pojawia się formularz edycji wybranej rezerwacji.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755541944447/3123c58a-6cd7-4d39-993e-773ebfdc3fa9.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu przycisku “Usuń rezerwację“ pojawia się popup z zapytaniem, czy administrator na pewno chce usunąć rezerwację.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755542049136/072be7d1-b209-4fd5-88a9-8da76b1fa1c9.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-37-ekran-panel-administratora-zakladka-sale-konferencyjne">3.7. Ekran “Panel Administratora“ zakładka “Sale Konferencyjne“</h3>
<p>Ekran „Panel administratora” – zakładka Sale Konferencyjne umożliwia przeglądanie wszystkich sal konferencyjnych w systemie. Administrator może przejść do edycji wybranej sali lub dodać nową salę konferencyjną, dzięki czemu łatwo zarządza zasobami dostępnymi dla użytkowników.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755542157234/24b4a18d-26c4-44d7-a911-74588d3c4593.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu przycisku “Edytuj“ pojawia się formularz edycji sali.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755542445704/88c813b1-8457-4d9d-9f6f-390322bc5f5f.png" alt class="image--center mx-auto" /></p>
<p>Po kliknięciu przycisku “+“ z opisem “Dodaj nową salę“ pojawia się formularz umożliwiający dodanie nowej sali konferencyjnej.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755542858343/b6dbb66a-973b-48b9-8903-d776931cdd90.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-4-walidacje-i-logika-biznesowa">4. Walidacje i logika biznesowa</h2>
<p>Sama struktura danych oraz ekrany aplikacji nie wystarczą, aby zapewnić poprawne działanie systemu rezerwacji. Kluczowym elementem jest odpowiednia logika biznesowa oraz walidacje, które gwarantują spójność i bezpieczeństwo danych, a także komfort pracy użytkowników. W projekcie zaimplementowano szereg mechanizmów kontrolnych – od sprawdzania poprawności wypełnianych formularzy, przez zapobieganie kolizjom terminów, aż po automatyczne zmiany statusów rezerwacji i ograniczenia związane z możliwością anulowania spotkań.</p>
<h3 id="heading-41-walidacja-dat-i-godzin-oraz-sprawdzanie-dostepnosci-sali">4.1. Walidacja dat i godzin oraz sprawdzanie dostępności sali</h3>
<p>Tworząc formularz dodawania nowej rezerwacji, należało zadbać o następujące zasady walidacji:</p>
<ul>
<li><p>Wszystkie pola oznaczone jako wymagane muszą zostać wypełnione.</p>
</li>
<li><p>Rezerwacja nie może być dodana z datą lub godziną w przeszłości.</p>
</li>
<li><p>Godzina zakończenia rezerwacji nie może być wcześniejsza niż godzina rozpoczęcia.</p>
</li>
<li><p>Rezerwacja nie może zostać dodana, jeśli w wybranej sali istnieje już inna rezerwacja w tym samym przedziale czasowym.</p>
</li>
</ul>
<p>Kod trigera OnSelect przycisku „Zatwierdź rezerwację” zawiera pełną logikę walidacji danych wprowadzanych przez użytkownika. W skrócie:</p>
<ol>
<li><p>Tworzone są zmienne DateTimeStart i DateTimeEnd, które wskazują na datę i godzinę rozpoczęcia oraz zakończenia rezerwacji na podstawie wartości wybranych w formularzu.</p>
</li>
<li><p>Sprawdzana jest poprawność dat:</p>
<ul>
<li><p>data rozpoczęcia nie może być przeszła,</p>
</li>
<li><p>data zakończenia musi być późniejsza niż data rozpoczęcia.</p>
</li>
</ul>
</li>
<li><p>Następnie następuje weryfikacja dostępności sali – w wybranym terminie nie może istnieć już aktywna rezerwacja dla tej samej sali.</p>
</li>
<li><p>Jeżeli wszystkie warunki zostaną spełnione, formularz jest zatwierdzany (SubmitForm), użytkownik otrzymuje powiadomienie o powodzeniu operacji, a formularz jest resetowany, przygotowując go do wprowadzenia kolejnej rezerwacji.</p>
</li>
</ol>
<pre><code class="lang-plaintext">//tworzenie zmiennych wskazujących na datę rozpoczęcia i zakończenia rezerwacji
Set(
    DateTimeStart,
    DateAdd(
        DateAdd(
            DateValue5.SelectedDate,
            Value(HourValue5.Selected.Value),
            TimeUnit.Hours
        ),
        Value(MinuteValue5.Selected.Value),
        TimeUnit.Minutes
    )
);
Set(
    DateTimeEnd,
    DateAdd(
        DateAdd(
            DateValue4.SelectedDate,
            Value(HourValue4.Selected.Value),
            TimeUnit.Hours
        ),
        Value(MinuteValue4.Selected.Value),
        TimeUnit.Minutes
    )
);

//sprawdzanie czy data rozpoczęcia nie jest datą przeszłą
If(
    DateTimeStart &lt; DateAdd(
        Now(),
        0,
        TimeUnit.Minutes
    ),
    Notify(
        "Rezerwacja nie może zostać dokonana z datą przeszłą.",
        NotificationType.Error
    ),

//sprawdzanie czy data rozpoczęcia nie jest późniejsza niż data zakończenia rezerwacji
    If(
        DateTimeStart &gt;= DateTimeEnd,
        Notify(
            "Data i godzina zakończenia muszą być późniejsze niż data i godzina rozpoczęcia.",
            NotificationType.Error
        ),

//sprawdzanie dostępności sali w wybranym terminie
        If(
            CountRows(
                Filter(
                    'Rezerwacje Sal',
                    SalaID.SalaID = Dropdown1.Selected.SalaID &amp;&amp; 'Start Rezerwacji' &lt;= DateTimeEnd &amp;&amp; 'Koniec Rezerwacji' &gt;= DateTimeStart &amp;&amp; 'Status Rezerwacji' &lt;&gt; [@'Status Rezerwacji'].Anulowana
                )
            ) &gt; 0,
            Notify(
                "W tym terminie sala jest już zarezerwowana",
                NotificationType.Error
            ),

//zatwierdzanie rezerwacji i resetowanie formularza
            SubmitForm(Form2);
            Notify(
                "Rezerwacja dodana pomyślnie!",
                NotificationType.Success
            );
            Set(
                varRec,
                Form2.LastSubmit
            );
            ResetForm(Form2);
        )
    )
)
</code></pre>
<h3 id="heading-42-filtrowanie-i-wyswietlanie-danych-w-galeriach">4.2. Filtrowanie i wyświetlanie danych w galeriach</h3>
<p>W aplikacji wykorzystano kilka galerii, dlatego istotne było, aby rekordy były wyświetlane w odpowiedni sposób, zgodnie z kontekstem danego ekranu i oczekiwaniami użytkownika.</p>
<p>Galeria w widoku <strong>Kalendarz rezerwacji</strong> wyświetla rezerwacje w porządku rosnącym według godziny rozpoczęcia, ograniczając widok do wybranego dnia w kalendarzu. Rekordy z anulowanymi rezerwacjami są pomijane, a dodatkowo można filtrować je po nazwie sali. Funkcja użyta do tego celu wygląda następująco:</p>
<pre><code class="lang-plaintext">SortByColumns(
    Filter(
        'Rezerwacje Sal',
        'Start Rezerwacji' &gt;= DateAdd(
            _dateSelected,
            0,
            TimeUnit.Days
        ) &amp;&amp; 'Start Rezerwacji' &lt; DateAdd(
            _dateSelected,
            1,
            TimeUnit.Days
        ) &amp;&amp; (SalaID.'Nazwa Sali' = Dropdown3_9.Selected.Value || IsBlank(Dropdown3_9.Selected.Value) &amp;&amp; 'Status Rezerwacji' &lt;&gt; [@'Status Rezerwacji'].Anulowana)
    ),
    "kr_startrezerwacji",
    SortOrder.Ascending
)
</code></pre>
<p>Galeria w widoku <strong>Moje rezerwacje</strong> wyświetla wszystkie rezerwacje dokonane przez zalogowanego użytkownika. Rekordy są sortowane w kolejności malejącej według daty rozpoczęcia, dzięki czemu najnowsze rezerwacje pojawiają się na górze listy. Dodatkowo użytkownik może filtrować widok według:</p>
<ul>
<li><p>statusu rezerwacji,</p>
</li>
<li><p>nazwy sali,</p>
</li>
<li><p>daty rozpoczęcia rezerwacji.</p>
</li>
</ul>
<p>Filtr uwzględnia tylko aktywne wartości w polach wyboru – jeśli filtr nie jest ustawiony, wszystkie odpowiadające rekordy są wyświetlane. Funkcja użyta do tego celu wygląda następująco:</p>
<pre><code class="lang-plaintext">SortByColumns(
    Filter(
        'Rezerwacje Sal',
        'Created By'.'Primary Email' = User().Email &amp;&amp; (IsBlank(Dropdown3_3.Selected.Value) || 'Status Rezerwacji' = Dropdown3_3.Selected.Value) &amp;&amp; (IsBlank(Dropdown3_4.Selected.Value) || SalaID.'Nazwa Sali' = Dropdown3_4.Selected.Value) &amp;&amp; (IsBlank(DatePicker1.SelectedDate) || 'Start Rezerwacji' &gt;= DateAdd(
            DatePicker1.SelectedDate,
            0,
            TimeUnit.Days
        ) &amp;&amp; 'Start Rezerwacji' &lt; DateAdd(
            DatePicker1.SelectedDate,
            1,
            TimeUnit.Days
        ))
    ),
    "kr_startrezerwacji",
    SortOrder.Descending
)
</code></pre>
<p>Galeria w widoku <strong>Panel Administratora – Rezerwacje</strong> umożliwia przeglądanie wszystkich rezerwacji w systemie. Rekordy są sortowane w kolejności malejącej według daty rozpoczęcia, dzięki czemu najnowsze rezerwacje pojawiają się na górze listy. Administrator może filtrować dane według:</p>
<ul>
<li><p>użytkownika, który utworzył rezerwację,</p>
</li>
<li><p>statusu rezerwacji,</p>
</li>
<li><p>nazwy sali,</p>
</li>
<li><p>daty rozpoczęcia rezerwacji.</p>
</li>
</ul>
<p>Filtry działają dynamicznie – jeśli pole wyboru jest puste, wszystkie odpowiadające rekordy są wyświetlane. Takie podejście pozwala administratorowi w szybki sposób odnaleźć interesujące rezerwacje i zarządzać nimi.</p>
<pre><code class="lang-plaintext">SortByColumns(
    Filter(
        'Rezerwacje Sal',
        (IsBlank(Dropdown3_7.Selected.Value) || 'Created By'.'Full Name' = Dropdown3_7.Selected.Value) &amp;&amp; (IsBlank(Dropdown3_5.Selected.Value) || 'Status Rezerwacji' = Dropdown3_5.Selected.Value) &amp;&amp; (IsBlank(Dropdown3_6.Selected.Value) || SalaID.'Nazwa Sali' = Dropdown3_6.Selected.Value) &amp;&amp; (IsBlank(DatePicker1_1.SelectedDate) || 'Start Rezerwacji' &gt;= DateAdd(
            DatePicker1_1.SelectedDate,
            0,
            TimeUnit.Days
        ) &amp;&amp; 'Start Rezerwacji' &lt; DateAdd(
            DatePicker1_1.SelectedDate,
            1,
            TimeUnit.Days
        ))
    ),
    "kr_startrezerwacji",
    SortOrder.Descending
)
</code></pre>
<h3 id="heading-43-ograniczenia-uzytkownikow">4.3. Ograniczenia użytkowników</h3>
<p>W aplikacji wprowadzono mechanizmy kontroli dostępu i ograniczenia funkcjonalne, mające na celu zapewnienie prawidłowego działania systemu oraz ochronę danych.</p>
<p>Pierwszą sytuacją uwzględnioną w ograniczeniach użytkowników jest brak możliwości rezerwacji sal oznaczonych jako „Niedostępna“. W takim przypadku przycisk „Zarezerwuj“ w widoku szczegółowym sali jest nieaktywny, a sama sala nie pojawia się w dropdownie formularza rezerwacji, uniemożliwiając jej wybór.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755603120622/f310dceb-13e9-47ba-95e1-4e25c05ae4b6.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755603180004/bc7fff98-b9f1-45aa-a9b0-8fe66e77c8cb.png" alt class="image--center mx-auto" /></p>
<p>Wartość właściwości Items dropdowna służącego do wyboru sali została ustawiona za pomocą poniższego wyrażenia. Dzięki temu w dropdownie wyświetlane są tylko sale dostępne, posortowane rosnąco według nazwy sali:</p>
<pre><code class="lang-plaintext">Sort(
    Filter(
        'Sale Konferencyjne',
        Dostępność = true
    ),
    'Nazwa Sali',
    SortOrder.Ascending
)
</code></pre>
<p>Kolejną sytuacją jest widoczność sekcji <strong>Panel Administratora</strong>. W trigerze OnStart aplikacji ustawiana jest zmienna isAdmin. Kod sprawdza, czy aktualnie zalogowany użytkownik należy do określonej grupy Office 365. Jeśli tak – zmienna isAdmin przyjmuje wartość true, jeśli nie – isAdmin = false.</p>
<pre><code class="lang-plaintext">Set(
    isAdmin,
    !IsBlank(
        LookUp(Office365Groups.ListGroupMembers("899c8404-8f9d-4df7-aedf-ed6704503ead").value,
        mail = User().Email
    )
));
</code></pre>
<p>W menu bocznym, dla przycisku “Panel Administratora”, wartość właściwości Visible została ustawiona na isAdmin. Dzięki temu przycisk jest widoczny wyłącznie dla użytkowników należących do określonej grupy administracyjnej.</p>
<p>Dla realizacji tego mechanizmu w aplikacji został dodany konektor Office365Groups, umożliwiający pobieranie członków grupy Office 365 i weryfikację uprawnień użytkownika.</p>
<p>Widok menu bocznego dla użytkownika z uprawnieniami administratora:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755604471065/a4224065-5712-49ad-824c-2d09197fe7ef.png" alt class="image--center mx-auto" /></p>
<p>Widok menu bocznego dla użytkownika bez uprawnień administratora:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755604408207/94c99253-9306-4d1a-9403-8f1b21232cd4.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-44-zarzadzanie-statusem-rezerwacji">4.4. Zarządzanie statusem rezerwacji</h3>
<p><strong>Status rezerwacji “Zakończona“</strong></p>
<p>Jednym z założeń aplikacji było zapewnienie automatycznej aktualizacji statusów rezerwacji po upływie terminu spotkania. W docelowym rozwiązaniu mechanizm ten powinien zostać zaimplementowany przy pomocy Power Automate Flow, który uruchamiałby się cyklicznie i zmieniał statusy rezerwacji w bazie danych. Niestety, w przypadku darmowego konta developerskiego Power Platform istnieją ograniczenia – Dataverse jest traktowany jako connector premium, a jego obsługa w Power Automate nie jest dostępna w tej wersji.</p>
<p>Z tego względu zachowanie to zostało zasymulowane w aplikacji. W zdarzeniu App OnStart umieszczono wyrażenie:</p>
<pre><code class="lang-plaintext">UpdateIf(
    'Rezerwacje Sal',
    'Status Rezerwacji' = [@'Status Rezerwacji'].Aktywna &amp;&amp; 'Koniec Rezerwacji' &lt; DateAdd(
        Now(),
        0,
        TimeUnit.Minutes
    ),
    {'Status Rezerwacji': [@'Status Rezerwacji'].Zakończona}
);
</code></pre>
<p>Kod ten aktualizuje wszystkie rezerwacje posiadające status “Aktywna“, których czas zakończenia już minął, ustawiając im status “Zakończona”.</p>
<p>Należy jednak zaznaczyć, że rozwiązanie to <strong>nie jest idealne</strong> – wyrażenie uruchamia się dopiero w momencie startu aplikacji, więc do tego czasu statusy pozostają niezmienione. Ma ono jedynie na celu zasymulowanie poprawnego działania aplikacji. W pełnej wersji (z wykorzystaniem Power Automate i connectorów premium) taki mechanizm mógłby działać w sposób w pełni automatyczny.</p>
<p><strong>Status rezerwacji “Anulowana“</strong></p>
<p>Kolejnym założeniem dotyczącym zarządzania statusem rezerwacji jest możliwość anulowania danego rekordu przez użytkownika, z zaznaczeniem, że anulowanie rezerwacji jest niedozwolone na mniej niż 2 godziny przed rozpoczęciem spotkania.</p>
<p>W rekordach rezerwacji użytkownika przycisk „Anuluj rezerwację” jest widoczny tylko wtedy, gdy rezerwacja posiada status “Aktywna” i do rozpoczęcia rezerwacji pozostało więcej niż 2 godziny. W przeciwnym razie przycisk jest nieaktywny, a obok niego wyświetlana jest ikonka „i” z podpowiedzią:<br /><em>„Nie można anulować rezerwacji — do rozpoczęcia pozostało mniej niż 2 godziny.”</em></p>
<p>Widok aktywnego przycisku “Anuluj rezerwację“:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755605324679/76792f0c-447f-4e50-a110-9d682131e5ad.png" alt class="image--center mx-auto" /></p>
<p>Widok nieaktywnego przycisku oraz podpowiedzi:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755605526352/b20329d9-9d21-475e-a485-7786c1bd6d10.png" alt class="image--center mx-auto" /></p>
<h3 id="heading-45-powiadomienia-i-integracja-z-kalendarzem">4.5. Powiadomienia i integracja z kalendarzem</h3>
<p>Kolejnym założeniem aplikacji było powiadamianie użytkowników o dokonanych rezerwacjach oraz przypominanie o nadchodzących spotkaniach. W idealnym scenariuszu mechanizmy te zostałyby zaimplementowane przy pomocy Power Automate Flow, które – w momencie utworzenia rekordu w tabeli <em>Rezerwacje Sal</em> – automatycznie wysyłałyby wiadomości e-mail z potwierdzeniami i przypomnieniami. Niestety, z uwagi na ograniczenia darmowego konta developerskiego (brak dostępu do connectorów premium, w tym Dataverse), konieczne było zastosowanie alternatywnego rozwiązania.</p>
<p><strong>Potwierdzenie rezerwacji</strong></p>
<p>Po pomyślnym zapisaniu formularza rezerwacji uruchamiane są dwie akcje w zdarzeniu OnSuccess:</p>
<ol>
<li>Wysłanie wiadomości e-mail z potwierdzeniem rezerwacji, zrealizowane przy użyciu connectora Office365Outlook i metody SendEmailV2(). Wiadomość zawiera dane takie jak: sala, termin spotkania oraz ewentualny cel rezerwacji. Dzięki formatowaniu HTML wiadomość jest czytelna i profesjonalna.</li>
</ol>
<pre><code class="lang-plaintext">Office365Outlook.SendEmailV2(
    User().Email,
    "Potwierdzenie rezerwacji: " &amp; LookUp(
        'Sale Konferencyjne',
        SalaID = varRec.SalaID.SalaID
    ).'Nazwa Sali',
    "Cześć " &amp; User().FullName &amp; ",&lt;br&gt;&lt;br&gt;" &amp; "Potwierdzamy dokonanie rezerwacji w czasie: " &amp; Text(
        varRec.'Start Rezerwacji',
        "dd mmmm yyyy HH:mm",
        "pl-PL"
    ) &amp; " - " &amp; Text(
        varRec.'Koniec Rezerwacji',
        "dd mmmm yyyy HH:mm",
        "pl-PL"
    ) &amp; "&lt;br&gt;Miejsce spotkania: " &amp; LookUp(
        'Sale Konferencyjne',
        SalaID = varRec.SalaID.SalaID
    ).'Nazwa Sali' &amp; If(
        !IsBlank(varRec.Cel),
        "&lt;br&gt;Cel spotkania: " &amp; varRec.Cel
    ) &amp; "&lt;br&gt;&lt;br&gt;Pozdrawiamy," &amp; "&lt;br&gt;Zespół Sala+",
    {IsHtml: true}
);
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755614059563/b6db3959-2b26-47c9-8c32-93bf2d58c30f.png" alt class="image--center mx-auto" /></p>
<ol start="2">
<li>Dodanie wydarzenia do kalendarza użytkownika (V4CalendarPostItem()) – zastosowane jako obejście problemu przypomnień. Każde wydarzenie ma ustawione automatyczne przypomnienie na 60 minut przed rozpoczęciem spotkania. W ten sposób udało się zasymulować mechanizm przypomnień, którego nie można było zrealizować poprzez Power Automate.</li>
</ol>
<pre><code class="lang-plaintext">Office365Outlook.V4CalendarPostItem(
    "Calendar",
    "Rezerwacja: " &amp; LookUp(
        'Sale Konferencyjne',
        SalaID = varRec.SalaID.SalaID
    ).'Nazwa Sali',
    Text(
        varRec.'Start Rezerwacji',
        DateTimeFormat.UTC
    ),
    Text(
        varRec.'Koniec Rezerwacji',
        DateTimeFormat.UTC
    ),
    "UTC",
    {
        Body: "Twoja rezerwacja sali konferencyjnej",
        Location: LookUp(
            'Sale Konferencyjne',
            SalaID = varRec.SalaID.SalaID
        ).'Nazwa Sali',
        IsReminderOn: true,
        ReminderMinutesBeforeStart: 60
    }
)
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755614001461/30fe60aa-51e8-4ddc-a0c3-4c2de89e9d0c.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-potwierdzenie-anulowania-rezerwacji">Potwierdzenie anulowania rezerwacji</h4>
<p>Podobny mechanizm działa przy anulowaniu rezerwacji – użytkownik otrzymuje wiadomość e-mail z informacją o anulowaniu spotkania. Wiadomość zawiera również przypomnienie o konieczności ręcznego usunięcia wydarzenia z kalendarza, ponieważ obecna implementacja nie obsługuje tej operacji automatycznie.</p>
<pre><code class="lang-plaintext">Office365Outlook.SendEmailV2(
    User().Email,
    "Potwierdzenie anulowania rezerwacji: " &amp; LookUp(
        'Sale Konferencyjne',
        SalaID = varRec.SalaID.SalaID
    ).'Nazwa Sali',
    "Cześć " &amp; User().FullName &amp; ",&lt;br&gt;&lt;br&gt;" &amp; "Właśnie anulowałeś rezerwację zaplanowaną na: " &amp; Text(
        varRec.'Start Rezerwacji',
        "dd mmmm yyyy HH:mm",
        "pl-PL"
    ) &amp; " - " &amp; Text(
        varRec.'Koniec Rezerwacji',
        "dd mmmm yyyy HH:mm",
        "pl-PL"
    ) &amp; " w " &amp; LookUp(
        'Sale Konferencyjne',
        SalaID = varRec.SalaID.SalaID
    ).'Nazwa Sali' &amp; ".&lt;br&gt;Nie zapomnij nanieść zmian w swoim kalendarzu." &amp; "&lt;br&gt;&lt;br&gt;Pozdrawiamy," &amp; "&lt;br&gt;Zespół Sala+",
    {IsHtml: true}
);
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755614254679/28b7a975-a00d-41a4-8df8-21ba4fadbffc.png" alt class="image--center mx-auto" /></p>
<h4 id="heading-ograniczenia-rozwiazania">Ograniczenia rozwiązania</h4>
<p>Należy podkreślić, że jest to rozwiązanie zastępcze – dodawanie wydarzeń do kalendarza nie było pierwotnym założeniem aplikacji, a jedynie sposobem na obejście braku możliwości korzystania z Power Automate. Dzięki temu użytkownicy otrzymują przypomnienie o rezerwacji, jednak proces nie jest w pełni zautomatyzowany (np. brak możliwości automatycznego usuwania wydarzeń po anulowaniu). W docelowym wdrożeniu, przy pełnych licencjach, mechanizmy te mogłyby zostać zrealizowane w sposób bardziej profesjonalny i kompletny.</p>
<h3 id="heading-46-uprawnienia-uzytkownikow">4.6. Uprawnienia użytkowników</h3>
<h4 id="heading-uprawnienia-administratora">Uprawnienia administratora</h4>
<p>Na potrzeby aplikacji zostały utworzone konta pracowników firmy w Azure Entra ID. Dodatkowo stworzono dwie grupy typu security – Employees oraz Admins. Na tej podstawie weryfikowany jest dostęp do wspomnianego wcześniej Panelu Administratora:</p>
<ul>
<li><p>użytkownicy przypisani do grupy Employees posiadają wyłącznie standardowe funkcjonalności aplikacji,</p>
</li>
<li><p>członkowie grupy Admins uzyskują dostęp do rozszerzonych funkcji administracyjnych.</p>
</li>
</ul>
<h4 id="heading-role-bezpieczenstwa-w-power-platform">Role bezpieczeństwa w Power Platform</h4>
<p>Kolejnym krokiem było nadanie odpowiednich uprawnień w Power Platform Admin Center. W tym celu stworzono dwie nowe role bezpieczeństwa (security roles):</p>
<ul>
<li><p>Employee Role – rola przypisana standardowym użytkownikom aplikacji.</p>
<ul>
<li><p>Dla tabeli Rezerwacje Sal: uprawnienia Create: User, Read: Organization, Write: User, Delete: None.</p>
</li>
<li><p>Dla tabeli Sale Konferencyjne: uprawnienia ograniczone do Read: Organization (tylko odczyt).</p>
</li>
</ul>
</li>
<li><p>Admin Role – rola przypisana administratorom.</p>
<ul>
<li><p>Dla tabeli Rezerwacje Sal: pełny dostęp (Create, Read, Write, Delete: Organization).</p>
</li>
<li><p>Dla tabeli Sale Konferencyjne: pełny dostęp do edycji danych (Create, Read, Write, Delete: Organization).</p>
</li>
</ul>
</li>
</ul>
<p>Dzięki temu udało się odseparować standardowych użytkowników od administratorów, zapewniając właściwy podział ról i bezpieczeństwo danych w aplikacji.</p>
<h2 id="heading-5-podsumowanie-i-wnioski">5. Podsumowanie i wnioski</h2>
<p>Celem projektu było przygotowanie aplikacji umożliwiającej rezerwację sal konferencyjnych w firmie, z uwzględnieniem logiki biznesowej, walidacji danych oraz kontroli dostępu użytkowników. W ramach realizacji zadania wdrożono mechanizmy: walidacji pól formularza, filtrowania i prezentacji danych w galeriach, obsługi statusów rezerwacji, a także ograniczeń w dostępie do funkcjonalności w zależności od roli użytkownika.</p>
<p>W trakcie pracy pojawiły się pewne ograniczenia wynikające z korzystania z darmowego konta Power Platform Developer – przede wszystkim brak możliwości wykorzystania Power Automate z konektorami premium. W związku z tym część funkcjonalności musiała zostać zasymulowana w kodzie aplikacji. Mimo tych ograniczeń udało się przygotować działające rozwiązanie, które w pełni spełnia początkowe założenia.</p>
<p>Wnioskiem z realizacji projektu jest potwierdzenie, że platforma Power Apps umożliwia w krótkim czasie zbudowanie użytecznej aplikacji biznesowej, nawet przy ograniczonych zasobach. Jednocześnie widać wyraźnie, że wykorzystanie pełnego środowiska produkcyjnego wraz z Power Automate oraz rozszerzonymi konektorami znacząco zwiększyłoby możliwości i pozwoliło na pełną automatyzację procesów.</p>
]]></content:encoded></item></channel></rss>