Pocro Vento Documentation

Technical documentation for the Events and Employee evaluation app

Version Commit: 7a580a0

Generated: 2026-10-12

1. Introduction

About this document

The purpose of this document is the definition of project “Development of the Events and Vozni park«. Employee evaluation part of the application will be added later.

The document describes and clarifies functionality that needs to be delivered in the new application.

This documentation is accompanied by design mock-ups created in Figma. The mock-ups serve solely as a visual aid for understanding the application flow and interface layout. The final appearance and behaviour of the application in production may differ from the mock-ups shown, depending on technical decisions made during the development phase.

Porsche Croatia d.o.o. is a general representative for the brands Volkswagen, Audi, SEAT, CUPRA, Škoda, MOON and Volkswagen Gospodarska vozila.

Legend

This a comment
This is a nice to have feature
This is a billed change request

App overview

Application management interface allows the users of the application to do most of their tasks. Core areas of the application management interface are:

2. General

2.1 Dictionary

Application

Refers to the custom-made application that is the subject of this document. Application management interface is private: only authenticated and authorized users can access it.

Brand

Refers to Porsche Croatia brands: Volkswagen (VW), Volkswagen Gospodarska vozila (VW GV), Škoda, Seat, Cupra, Audi. Every brand has a name and a code.

Client

Porsche Croatia d.o.o.

User

Each authenticated user of the application.

Brand manager

Users of the app that mostly manages events.

Promotor

Promotors working on events that scan the QR codes of invitees and manages the signing of the document.

Invitees

Visitors of the event that need to sign the documents.

2.2 Legend

Items marked as nice to have are implemented if they require little effort. If assessed as significant effort, they should be estimated separately.

Nice to have functionality.

Items marked as change request indicate additional scope. These items should be estimated separately.

Billed change request.

2.3 About this document

The purpose of this document is the definition of project “Development of the Events and Vozni park”. Employee evaluation part of the application will be managed separately.

The document describes and clarifies functionality that needs to be delivered in the new application.

This documentation is accompanied by design mockups created in Figma. The mockups serve solely as a visual aid for understanding the application flow and interface layout. The final appearance and behaviour of the application in production may differ from the mockups shown, depending on technical decisions made during the development phase.

Porsche Croatia d.o.o. is a general representative for the brands Volkswagen, Audi, SEAT, CUPRA, Skoda, MOON and Volkswagen Gospodarska vozila.

2.4 Technical limitations

The application should be optimized for versions of Chrome and Firefox on Windows 11, Linux; Chrome, Firefox and Safari on macOS; released in the last 6 months. Older versions should either work with reduced guest experience or block access to the booking web application with recommendation to upgrade the browser.

All code must reside in git.

All application configuration (database access, passwords, keys, routes, SMTP server, etc.) must be stored in a single configuration file. This file must not be stored in GIT. This should allow the same software to run on the production and test environment (same package, bin, exe, etc.).

Must use fully automated continuous integration / continuous deployment with two environments:

  • production
  • development

2.5 Overview

Application management interface allows the users of the application to do most of their tasks. Core areas of the application management interface are:

  • Authentication/authorization (known as “login”)
  • User management
  • Event management
  • Document management
  • Invitee management
  • Signing the documents at the spot
  • Reservations and form signing for Vozni park

The application will be in Croatian.

Parts of application are made responsive, so they are accessible on the mobile phone.

Application works online. However, part of the app - events list, invitees on the event and starting and ending the activities (test drives) should be available and work also offline. The promotor should be able to start and finish the test drive for a specific invitee, information should be saved, even if the app is offline. If multiple users are doing this, the activity would be saved multiple times.

2.6 Authentication/authorization

Authentication/authorization is done by an external service (such as Azure B2C). Details are dependent on the selected service.

Core features:

  • Users must be authenticated and authorized to access the admin part.
  • Users must be authenticated with email and password.
  • Registration of users is not possible.
  • Sign in.
  • Sign out.
  • Forgotten password.
  • Change password.

2.7 User management

User management is only accessible to the user with the role “user-manager”. User management is tightly integrated with the chosen authentication and authorization platform.

The user list displays the following user data:

  • Email
  • Assigned roles

The list has no pagination and no filtration.

User-manager can:

  • Add new users.
  • Assign email.
  • Assign a random password, but not store it. Expected process is that the new user will click “forgotten password” and reset it.
  • Edit existing users.
  • Assign roles.
  • Delete users.

A deleted user loses access to the application, but his/her records must be kept in reservation timeline, where we display which user made a status change.

2.8 User roles

Types of roles:

Admin

Admin has access to all parts of the application (Events, Vozni park, Inventar, Assessment i školovanja, user management).

Brand managers (uvoznik)

Brand managers can view events, create events, manage invitees, select vehicles for events, and manage evaluations.

Managers of Vozni park

Managers of Vozni park have access to Vozni park part of the application.

Evaluators (partners)

Evaluators can fill out the forms for the assessments. Evaluators are part of Employee evaluation, they do not have access to Events / Vozni park part of the app.

Promotors

Promotors at the sign-in counter scan QR code, take consents from invitees and track their activities. They can also access list of events and invitees for a specific event and see the consents they have already signed with the timestamp.

Promotors only have access to events application.

They should not have the option to edit or download documents.

User-manager

For managing users there is an extra role called “user-manager”. User-manager can manage users (add, delete, or edit users).

2.9 Language

The app interface is in Croatian language. This documentation is in English.

2.10 Offline functionality

The entire app is a web app that works with an internet connection. However, parts of the application should be functional without internet connection.

Offline availability per event

Each event has its own offline toggle that lets the user choose which events are downloaded and kept available for offline use on their device.

The toggle is shown on the Event detail page to every user who can access that event. It is not restricted by role. The toggle is off by default and must be enabled manually for each event.

When the toggle is enabled:

  • All data for the event is downloaded and stored locally on the device.
  • The event is clearly marked as available offline.
  • When the user is offline, the Events list shows only events selected for offline use.

When the toggle is disabled, the locally stored data for that event is deleted from the device.

If the event has offline changes that have not yet been synchronized with the server, such as activities started or finished while offline, the toggle cannot be disabled. The toggle remains enabled, the locally stored data is preserved, and the following toast notification is shown: “Offline način nije moguće isključiti dok se ne sinkroniziraju svi podaci.”

Example:

  • Event A has the offline toggle enabled, so the event and its data are available without an internet connection.
  • Event B has the offline toggle disabled, so its data is not stored locally and it is not shown in the Events list while offline.
  • The user disables the offline toggle for Event A. Its local data is deleted if there are no unsynchronized changes waiting to be sent to the server.

2.11 Brand specifics

The application is used by multiple brands; each brand has their own logotype. The core application remains the same, with identical functionalities and features.

Data in the app is not restricted by brand. All users see information from all the brands, scoped only by which part of the app they have access to (e.g. Events vs. Vozni park). Within that scope, they can filter the view down to their own brand if they want to. *NEEDS TO BE CONFIRMED WITH CUSTOMER

We show the brand logotype on the event detail page, PDF generated documents, and in the email invitees receive.

2.12 Backend: Data retention (Assessments and BTC)

Backend task for the 2-year data retention rule – a daily timer function that deletes or anonymises Assessments, BTC školovanja and BTC certificiranja whose end date is more than 2 years ago (soft-deleted records included), with their child rows and signature files. Check the end date per record type, decide delete vs anonymise, implement, and verify.

Docs: Assessments – Data retention, BTC – Data retention Spec: none. This is a background job with no API endpoint. DB: all Assessments and BTC tables in api/database/create_schema.sql

Data of Assessments, BTC školovanje and BTC certificiranje is kept for 2 years after the record’s end date. Deleting a record in the app is only a soft delete (DeletedAt), so this job is what finally removes the data.

1. Double check API spec and DB structure

  • End date per record type (the 2 years count from this date):

    • Assessment → Assessments.Date
    • BTC školovanje → Modul 4 BtcTrainingModules.EndDate
    • BTC certificiranje → BtcCertifications.EndDate
  • Školovanje without a Modul 4 end date: module dates are optional, so some školovanja have no end date (they stay Aktivni) and would never expire.

    Not specified: which date counts for a školovanje without a Modul 4 end date. Options: the latest date entered on any module; CreatedAt; or never expire. A školovanje with no module dates at all needs the same decision.
  • Soft-deleted records (DeletedAt set) follow the same rule, by their end date, not by the deletion date. They could only be deleted while upcoming, so they are always kept for at least 2 years.

  • Delete or anonymise:

    Not specified: should expired records be deleted or anonymised? Delete: remove the record and all its child rows, plus the signature files in blob storage. Anonymise: keep the records and grades (e.g. for statistics), but clear the personal data: candidate FullName and Email, the free-text notes (AssessmentNotes), the komisija FullName / UserId, and the signature files (they show signatures). Please decide.
  • Scope: only Assessments and BTC (školovanje and certificiranje).

    Not covered: the other sections (Events with invitees, consents and signed documents; Vozni park with reservations, zapisnici and photos). Do they need a retention rule too? If yes, it gets its own task.
  • Boundary: a record expires when its end date is before today (Europe/Zagreb) minus 2 years. A record that ended exactly 2 years ago is kept until the next day. Confirm.

  • DB: check the indexes support the daily query: IX_Assessments_Date exists; there is no index on BtcTrainingModules.EndDate or BtcCertifications.EndDate. Add them if needed.

2. Implementation

  • Timer function: a new TimerTrigger function, once a day at night (e.g. 0 0 2 * * *), following EventInviteeEmailTimerFunction (try/catch, log errors, the next run picks up whatever failed).
  • Cut-off date: today in Europe/Zagreb (the same helper as the BTC status) minus 2 years.
  • Per section, one repository method that finds the expired records and processes them in small batches, one transaction per record, so one failure doesn’t block the rest.
  • Delete (if chosen): child rows in FK order, since there is no ON DELETE CASCADE:
    • Assessments: AssessmentGrades, AssessmentNotes, AssessmentWrittenTestGrades → AssessmentBoardMembers, AssessmentCandidates → Assessments
    • BTC školovanje: BtcTrainingAttendance, BtcTrainingSignatureFiles → BtcTrainingModules, BtcTrainingCandidates → BtcTrainings
    • BTC certificiranje: BtcCertificationGrades, BtcCertificationWrittenExamScores → BtcCertificationBoardMembers, BtcCertificationCandidates → BtcCertifications
  • Anonymise (if chosen): clear the fields agreed in step 1 and mark the record as anonymised so it isn’t processed again (e.g. an AnonymisedAt column).
  • Signature files: delete the blobs in btc-signatures after the DB transaction commits. A missing blob doesn’t fail the run. Uses the DeleteAsync added in Backend: Potpisi page.
  • Logging: log how many records of each type were processed, and every failure with the record id (no personal data in logs).

3. Verify implementation

  • An assessment, a školovanje and a certificiranje that ended 2 years and 1 day ago are processed. Ones that ended exactly 2 years ago, or later, are untouched.
  • A soft-deleted record past the cut-off is processed. One before it is untouched.
  • After a delete run: no orphan child rows, and the signature blobs are gone.
  • Running the job twice in a row changes nothing the second time.
  • A failure on one record (e.g. a blob delete error) doesn’t stop the other records, and the record is processed on the next run.
  • The školovanje-without-end-date case behaves as decided in step 1.

Done when

Every night, Assessments and BTC data older than 2 years after its end date is deleted or anonymised, as decided, without manual work.

2.13 Milestones

1. Platform foundation and delivery setup

Establish the shared project foundation: application architecture, core data model, environment/config handling, CI/CD, base API and web app structure, shared layout/navigation, and baseline error handling and browser support.

Scope:

Estimate: 8-12 man-days

2. Authentication, authorization, and user management

Integrate external authentication, implement sign in/out and password flows, protect routes and APIs, define role-based access, and build user CRUD with role assignment for the user-manager role.

Scope:

Estimate: 8-12 man-days

3. Vehicle inventory

Deliver vehicle CRUD, search/filter/pagination, archive and delete rules, vehicle statuses, vehicle detail/edit screens, and booking history visibility needed by both Events and Fleet.

Scope:

Estimate: 10-15 man-days

4. Fleet reservations and availability calendar

Build the fleet calendar, grouped vehicle availability view, reservation CRUD, conflict detection, prefilled booking flows from the calendar, and reservation validations tied to vehicle availability.

Scope:

Estimate: 10-15 man-days

5. Fleet pickup and return protocols

Implement the two-step pickup and return forms, draft persistence, signatures, attachments, PDF generation, protocol list/downloads, email sending, and protocol status transitions from draft to pickup to return.

Scope:

Estimate: 15-20 man-days

6. Events core management

Deliver event list, create/edit/archive event flows, event detail, vehicle assignment to events, event metadata and questionnaires, and per-event invitee management including the main list/detail management surfaces.

Scope:

Estimate: 15-20 man-days

7. Events operational workflows

Implement QR-based invitee lookup, document signing, signed PDF availability with role restrictions, single- and dual-timestamp activity tracking, activity logs, and vehicle reassignment logic during overlapping test-drive activity flows.

Scope:

Estimate: 15-20 man-days

8. Cross-domain integrations, offline support, and hardening

Finish automatic event mailings, general invitee/document/activity lists where included in release scope, offline support for the documented event flows, sync/reconnect behavior, permissions hardening, QA, and production-readiness work.

Scope:

Estimate: 15-20 man-days

3. Events

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=26-1766&t=H7jz4hMIphTvr0OG-1

3.1 Event management

Event management is done by brand managers.

3.2 Events list

Events list is visible to brand managers and to promotors.

The list contains the following information:

  • Name of the event
  • Event brand
  • Location
  • Date of start
  • Date of end
Status: future, active, past. Status is time-based.

User can:

  • View all events
  • Search events by name or location
  • Filter events by brand
  • Filter events by date
  • Filter to show only archived or only non-archived events
  • Add new events
  • View detail of event
  • Edit existing events (only for non-archived events)
  • Delete existing events if no invitees were imported for that event. Modal window for confirmation appears.
  • Archive/unarchive event

When the event is finished, brand manager checks if all information on the event is correct and archives it.

Archived events cannot be edited anymore. They need to be unarchived before editing.

The default view excludes archived events. Vehicles added to an archived event are considered free vehicles, so they can be reserved or added to another event.

A filter “Prikaži arhivirane događaje” shows only archived events.

3.3 Adding new event

The following data can be added for each event:

  • Brand - selected on a dropdown (mandatory field)
  • Name of the event - text field (mandatory field)
  • Start day / time - selection on a calendar, time selector (mandatory field)
  • End day / time - selection on a calendar, time selector (mandatory field)
  • Location - text field (mandatory field)
  • Documents for the event - selection from a predefined list
  • Activities for the event - selection from a predefined list
  • Questionnaires list - name and URL fields (optional field)
  • Questionnaire results list - name and URL fields (optional field)
  • Notes - text field (optional field)
  • Vehicles on the event - selection from a predefined list
  • Invitees - event manager manually enters invitees

For some documents, event-specific content should be added.

For some activities, name of the module can be added.

3.4 Event detail

Event detail shows the event setup and links to related event data.

The page shows the following information:

  • Event name
  • Edit event button
  • Brand logotype
  • Event info summary: brand, location, start date, start time, end date, end time
  • Created by (name of brand manager that created the event)
  • Invitees card with count, linking to invitee list
  • Vehicles card with count, linking to vehicle list
  • Questionnaires (expandable section)
  • List of documents (expandable section)
  • List of activities (expandable section)
  • Notes (expandable section)

Questionnaires section shows questionnaire and questionnaire result names and URLs that were added while creating the event.

List of documents shows documents that were selected while creating the event.

List of activities shows activities that were selected while creating the event.

Notes section shows notes that were added while creating the event.

Event status badge (Active).

3.5 Invitee management

Invitees are added on a specific event by the brand manager.

On the invitee profile for the event, the app tracks which documents the invitee has signed and which activities he has taken.

A general invitee list is also available, where all invitees are consolidated and identified by their email address.

3.6 List of invitees on event

We have invitee list under each event. Invitee list is visible to brand managers and promotors, who can also see that event.

New invitees are added manually by brand manager.

The list contains the following information:

  • Name and surname
  • Company
  • Email

User can:

  • View all invitees for the specific event
  • Search invitees by name, company or email
  • Add new invitee to the event
  • View invitee detail
  • Edit existing invitee using the same form as Add new invitee
  • Delete invitee from the event. Modal window for confirmation appears.
When the event is archived, invitee list cannot be edited anymore. “Dodaj uzvanika”, “Uredi uzvanika” and “Ukloni uzvanika” buttons are disabled.
We add filters to list of invitees on the event. User can filter by documents that invitees sign. On the list we show only those invitees that signed that document. User can also filter by activities that user took. On the list we show only those invitees that took a specific activity. This is information needed for auditor.

3.7 Adding new invitee

Brand manager adds new invitee on the specific event.

Required data:

  • Name and surname - text field (mandatory field)
  • Company - text field (mandatory field)
  • Invitee’s residence/address - text field (optional field)
  • E-mail - email field (mandatory field), unique identifier for an invitee
  • Telephone number - telephone field (optional field)
  • Partner ID - number field (optional field)
A notification about emailing the invitee is added. This is connected to the mailings change request.

3.8 Invitee detail

Invitee detail page is accessible from the invitee list on the event or via QR code invitee receives by email.

It displays all information related to a specific invitee for that specific event.

The page contains the following information:

  • Invitee name and surname
  • Company, address, email, telephone number and partner ID shown as a summary line below the name
  • Show QR code button. It opens a modal with QR code, event name and invitee name below the code.
  • Documents section
  • Activities section
  • Activity log section

Documents section shows a list of all documents the invitee needs to sign for this event and documents the invitee already signed.

Per document we show:

  • Document name
  • Status “Signature required” or “Signed”
  • Action “Sign” if not yet signed, or Download if already signed

Generated PDF content is defined in Document signing.

Activities section shows a list of all activities assigned to this invitee for this event. All activities are the same for all invitees on one event.

For single-timestamp activities:

  • Activity name
  • “Check-in” button
  • Status “Checked in” if completed. The button is no longer shown after completion.

For dual-timestamp activities:

  • Activity name
  • “Start activity” button when the activity has not yet started
  • “End activity” button once the activity is started

Activity log shows a chronological log of all completed activities, sorted in reverse chronological order. The most recent activity is shown at the top.

For single-timestamp activities we show activity name and start time.

For test drives we show activity name, registration plate, start time, end time, and comment.

3.9 Invitee list general

Brand managers can access a general invitee database and see which events an invitee already attended. Unique ID of the invitee is email. This request needs to be included.

On the list we show:

  • Name and surname
  • Company
  • E-mail address

Clicking the invitee opens an expandable section with all attended events:

  • Name of the event
  • Brand of the event
  • Location
  • Start date
  • Status
  • Link to detail page of the invitee on the specific event

3.10 Document management

Document management is done by developers.

New consent documents need to be sent to the developers to be implemented into the application.

3.11 Document signing

Document signing is done from a signing page.

The page shows prefilled information. Invitee must add additional information to the document, checkmark that they agree with terms and conditions, and sign the document.

When invitee signs the document, the app generates a PDF based on the following information:

  • Invitee that this document belongs to
  • Name of document the invitee signed
  • Brand logotype
  • Consent checkbox info
  • Name of the event
  • Time period of the event
  • Location of the event
  • Event-specific content defined by the brand manager when creating the event, for example service provider name
  • Dynamic content by the invitee, for example name or email
  • Manually inputted content by the invitee, for example license permit number
  • Invitee signature
  • Timestamp of signature
  • Document body

A PDF is generated based on this information. The PDF displays dynamic and manually inputted content. Some content can have a remark, such as “Hereinafter referred to as service provider”.

PDF is available for download on invitee detail page. Only brand managers can download the PDF. Promotors should not see the download button.

For signing the document, we do not use any certified services for signature.

Every new event needs to have a new signature from the invitees. Consents are not valid for future events.

Event-specific content should be included in generated documents when it is defined by the brand manager while creating the event, for example service provider name.

3.12 General list of documents

General list of documents is available in the app. Brand manager can view the list and open a detail of each document. On detail, it should be visible what dynamic and manual content is included in the document and what fixed content comes from the template. The same screen that invitee sees when signing the document can be reused.

List of documents in the first phase of the app:

  • Disclaimer
  • Izjava o suglasnosti

3.13 Activity management

Activity management tracks what invitees do on a specific event.

List of activities is editable only by developers. New activities need to be sent to developers to be implemented into the application.

Activities are selected when event is created and then shown on invitee detail.

There are two types of activity tracking:

  • Single-timestamp activities
  • Dual-timestamp activities

3.14 Activities tracking (single-timestamp activities)

Single-timestamp activities require only one check-in action to be recorded.

Example:

  • Registration

When a promotor clicks the “Check-in” button on the invitee profile, a page for tracking activity opens with current date and time preselected. Promotor can change the date and time, add comment and click “Spremi”.

Once completed, the activity status changes to “Evidentirano” and the action button disappears. The activity cannot be edited after check-in.

The activity is marked as completed and timestamp is saved.

If activity has a custom name added during event setup, the custom name is shown on the list of activities.

Activity log:

The following information is recorded and saved on the invitee profile for each single timestamp activity (Popis aktivnosti):

  • Activity name, with custom name if it is added
  • Date and time of check-in
  • Comment

3.15 Activities tracking (dual-timestamp activities)

Dual timestamp activities require both a start and an end action to be recorded. Test drive is the primary example of this activity type.

Example:

  • Test drive

Starting the activity:

  • Promotor clicks “Pokreni aktivnost” on Test drive on the invitee profile.
  • A page for tracking activity opens with a vehicle field, start date and time, and comment.
  • The vehicle is selected from vehicles assigned to the event. Options show registration plate - brand - vehicle type.
  • Start date and time are pre-filled with current date and time and can be changed by the promotor.
  • Comment is optional.
  • Promotor clicks “Spremi” to confirm.
  • Activity on invitee profile is marked as started and the button changes to “Završi aktivnost”.

If the selected vehicle is currently assigned to another invitee, notification appears:

“Ovo vozilo ima aktivnost u tijeku s korisnikom [ime korisnika]. Ako dodijelite ovo vozilo novom uzvaniku, ta aktivnost će automatski biti završena.”

If the promotor confirms, the other invitee’s test drive is automatically marked as finished, end time is set to now, and the new test drive starts for the new invitee.

Ending the activity:

  • Promotor clicks “Završi aktivnost” on the invitee profile.
  • A page for tracking activity opens with a vehicle field, end date and time, and comment.
  • The vehicle field shows the selected vehicle as registration plate - brand - vehicle type and is read-only.
  • End date and time are pre-filled with current date and time and can be changed by the promotor.
  • Comment is optional.
  • Promotor clicks “Spremi” to confirm.
  • Activity is marked as completed and added to the activity log.

Invitee can drive the same car more than once.

Activity log:

All completed test drives are listed in the activity log (Popis aktivnosti) on the invitee profile.

Each entry shows:

  • Activity name
  • Registration plate
  • Start date and time
  • End date and time
  • Comment

An unlimited number of test drives can be tracked per invitee.

Activity information cannot be edited after saving.

3.16 General list of activities

General list of activities is available in the app. Brand manager can view the list and, if possible, open a detail of each activity. The same screen that promotor sees when tracking activities can be reused.

List of activities in the first phase of the app:

  • Test drive (Testna vožnja) - dual-timestamp activity
  • Registration (Registracija) - single-timestamp activity
  • Modul 1 + custom field for module name - single-timestamp activity
  • Modul 2 + custom field for module name - single-timestamp activity

When setting up the event, brand manager can choose from this list of activities to be added to the event.

3.17 Vehicle management

Vehicle management on events is done through the general vehicle inventory.

Vehicles on the event are selected from the general vehicle inventory. Brand manager adds vehicles to the event by selecting them from the list.

Vehicle list on event displays all vehicles assigned to the specific event.

The list contains the following information per vehicle:

  • Brand
  • Model
  • Registration plate
  • Chassis number
  • Color

Each vehicle row can be expanded to show the driver history log for that vehicle - a list of all invitees who drove that vehicle during the event, with start time, end time and comment for each entry.

User can:

  • View all vehicles on the event
  • Edit the vehicles list

When adding vehicles to the event, brand manager is taken to a separate page showing the full vehicle inventory. All vehicles are shown, from Događaji and Vozni park sections. Archived vehicles are not shown.

The list contains the same columns as above, with an additional “Status za vrijeme događaja” column showing the status of each vehicle for the period of the event, for example “Zauzeto” if the vehicle is already taken for another event.

Only vehicles with status “Aktivno” can be selected. Vehicles with status “Zauzeto” or “Neispravno” are visible in the list but cannot be selected.

User can:

  • Search vehicles by brand, model or registration plate
  • Select one or multiple vehicles by checking the checkbox next to each vehicle
  • Save the selection by clicking “Spremi”. The number of selected vehicles is shown on the button, for example “Spremi 10”.

Vehicles added to an event are also shown on Vozni park calendar as event usage.

When the event is archived, vehicles list cannot be edited anymore and “Dodaj vozila” button is disabled.
If there is an easier way to solve selecting vehicles for an event, the flow can be adjusted.

3.18 QR Codes

  • QR codes are generated by the app

  • Each invitee has a unique QR code for each event

  • QR code is accessible on the invitee detail page in the app

  • When the QR code is scanned, the invitee profile for the specific event is shown

  • User must be signed in to see the invitee profile

  • After the event is done, the QR codes can stop working

3.19 Mailings

Mailings are used to send event-related information to invitees.

Three days before the event, automatic mailing is sent to all invitees on the event.

The mailing contains QR code of each specific invitee.

If invitee is entered after this timeframe, they receive the email right after they are added to the event.

Mailings are specific per brand. Brand logotype is shown in the email.

Mailings are part of the change request scope.

3.20 Documents

This section describes documents that were in use before using the app and how they will be managed in the app.

Documents are selected on event creation from a predefined list.

Some documents need event-specific content added by the brand manager.

Documents that require signatures are signed by invitees during the event. After signing, the app generates a PDF document with event, invitee, brand and signature information.

Documents described in this section:

  • Disclaimer
  • Izjava o suglasnosti
  • Potpisna lista
  • Popis vozača - potpisna lista
  • Preuzimanje vozila za školovanje

3.21 Disclaimer

There are several versions of this document. All different versions will be prepared. Each one will be available as a separate document that brand manager can add to event.

This document will have to be signed by the invitee to follow the same document signing logic that is used to manage documents.

When invitee is signing the document, we display:

  • Name of document the invitee is signing
  • Time period of the event (disabled field)
  • Name of the event (disabled field)
  • Location of the event (disabled field)
  • Name of the invitee (disabled field)
  • Document body
  • Consent checkbox

On the generated PDF document, we display:

  • Name of document the invitee is signing
  • Time period of the event
  • Name of the event
  • Location of the event
  • Name of the invitee
  • Document body
  • Timestamp of signature
  • Invitee signature

3.22 Izjava o suglasnosti

When invitee is signing the document, we display:

  • Name of document the invitee is signing - “Izjava o suglasnosti”
  • Time period of the event (disabled field)
  • Name of the event (disabled field)
  • Location of the event (disabled field)
  • Name of the invitee (disabled field)
  • Invitee’s residence/address - text field, prefilled from invitee detail page, can be edited
  • Document body
  • Consent checkbox

On the generated PDF document, we display:

  • Name of document the invitee is signing
  • Time period of the event
  • Name of the event, printed with the remark: u daljnjem tekstu: “događaj”
  • Location of the event
  • Name of the invitee, printed with the remark: u daljnjem tekstu: “sudionik”
  • Service provider name (Naziv proizvođača / rent-a-car društva / leasing društva) - text field; printed at the top of the document with the remark: u daljnjem tekstu: “ponudnik storitev”
  • Invitee’s residence/address (prebivalište)
  • Document body
  • Timestamp of signature
  • Invitee signature

3.23 Potpisna lista

“Potpisna lista” is managed in the application under activities - registration.

This document is not shown as a document the invitees need to sign.

3.24 Popis vozača - potpisna lista

“Popis vozača - potpisna lista” is managed in the application under activities - test drives.

This document is not shown as a document the invitees need to sign.

3.25 Preuzimanje vozila za školovanje

“Preuzimanje vozila za školovanje” is managed with pickup / return records in the Vozni park part of the app.

This exact document will not exist as a separate document in the app.

4. Vozni park

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2027-6740

Vozni park is a separate module for managing the company’s shared vehicle fleet: vehicles used for rentals, journalist test cars, and other non-Events purposes. It is opened from the side menu: Vozni park → Kalendar zauzetosti / Zapisnici. The page header shows “Vento | Vozni park”.

The vehicles come from the general vehicle inventory (side menu: Inventar). Vozni park does not have its own vehicle list. See Vehicle Inventory.

Reservations

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2027-6740

The Kalendar zauzetosti shows when each vehicle is reserved in Vozni park, used on an event, or out of order. Reservations are planning only: they are not connected to the actual pickup and return times in the Zapisnici.

A reservation has a start and an end date-time. When no time is given, the reservation covers whole days: it starts at 00:00 on the first day and ends at 23:59 on the last day.

Pages:

Zapisnici

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2059-16886

A zapisnik (vehicle handover record) documents the pickup and the return of a vehicle. It is not linked to a reservation: when it is started from a reservation, the reservation data is only copied into it. Later changes to the reservation (or its deletion) do not change the record, and the record does not change the reservation.

Each record goes through these statuses:

The pickup and the return are each signed in the app by the user and by Porsche Croatia. Each has its own read-only view and its own PDF, and a copy is sent to the user’s e-mail.

Pages:

Roles

Manager of Vozni park

Admin

Brand managers and Promotors have no access to Vozni park.

Everyone with access to Vozni park can do everything in it, so the pages have no per-role permission tables.

Vehicle groups on the calendar

Each vehicle has a Vrsta korištenja (Vozni park or Događaji) and, for Vozni park vehicles, a Namen (e.g. IT, Postprodaja, Novinarji, Veliki kupci). Both are set in the inventory (Add new vehicle) and only decide where the vehicle is shown on the calendar. Any vehicle can be reserved in Vozni park or used on an event.

After saving

After saving or creating anything in Vozni park (a reservation, a draft, a record), the user returns to the previous view (the calendar in the month it showed, or the Zapisnici list), and a success toast is shown.

4.1 Adding new reservation

The "Nova rezervacija" page, opened from "+ NOVA REZERVACIJA" on the Kalendar zauzetosti (empty) or by clicking an empty calendar cell (Vozilo and Od prefilled). Fields are Vozilo, Od and Do (all mandatory; whole days, 00:00–23:59), and Tvrtka, Ime i prezime and Email / Telefon (optional, but Tvrtka or Ime i prezime must be filled in). The period cannot overlap another reservation or event of the same vehicle, and a Neispravno vehicle cannot be reserved from today on ("Vozilo nije dostupno u odabranom razdoblju."). Saving returns to the calendar with a success toast and a new blue bar.

Users with access to Vozni park can add a reservation. The page can be opened in two ways from the Kalendar zauzetosti:

  • The “+ NOVA REZERVACIJA” button at the top of the calendar – Vozilo, Od and Do are empty and filled in manually.
  • Clicking an empty cell on the calendar – Vozilo is prefilled from the row and Od from the day that was clicked.

Figma (note on the empty-cell click): https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2055-7250

Conflict for the assignee to check: the Figma note says the empty-cell click opens a “Nova rezervacija” modal window, but the Figma screen is a full page with a back arrow. These docs follow the screen: a full page in both cases.

Page title: “Nova rezervacija”, with a back arrow that returns to the calendar. Breadcrumb: Kalendar zauzetosti.

Podaci o rezervaciji

(Figma has the section title “Podaci rezervacija”, which is not correct Croatian.)

  • Vozilo – searchable dropdown, mandatory. Each option shows brand and model, with the registration plate in grey (e.g. “SEAT Leon FR ST-789-CC”). Archived vehicles are not shown.
  • Od – date picker, mandatory. Start of the reservation.
  • Do – date picker, mandatory. End of the reservation.

Reservations are stored as date-times. The form has dates only, so the reservation starts at 00:00 on the Od day and ends at 23:59 on the Do day.

Not specified: date validation. Recommended: Do cannot be before Od. A one-day reservation (Od = Do) is allowed. Past dates are allowed, because past reservations can also be edited.

Podaci o vozaču

  • Tvrtka – text field, optional, see the rule below (placeholder “Unesite naziv”). Company the vehicle is reserved for.
  • Ime i prezime – text field, optional, see the rule below (placeholder “Unesite ime i prezime”). Person the vehicle is reserved for.
  • Email / Telefon – text field, optional (placeholder npr. email@primjer.hr). One contact field: either an e-mail address or a phone number.

(In Figma, the Ime i prezime placeholder repeats “Unesite naziv”, and the Email / Telefon placeholder has the English “eg.” instead of “npr.”.)

At least one of Tvrtka or Ime i prezime must be filled in. If both are empty, the error message is: “Barem jedno od polja Tvrtka ili Ime i prezime mora biti ispunjeno.”

To mark a vehicle as out of order for a set period, the user adds a normal reservation for that period and writes “Odjava” / “Out of order” or similar in Ime i prezime. It is shown in blue like any other reservation. (A vehicle that is out of order with no end date is set to “Neispravno” in the inventory instead.)

Availability

A vehicle is not available in the selected period if:

  • the period overlaps another Vozni park reservation of the same vehicle, or
  • the period overlaps an event the vehicle is added to, or
  • the vehicle has status Neispravno in the inventory and the period includes today or a later day (Neispravno applies from today on).

Then the reservation is not saved and the error message is: “Vozilo nije dostupno u odabranom razdoblju.”

Vozni park reservations and event bookings are kept together as vehicle reservations, and no two vehicle reservations of the same vehicle can overlap. Because a reservation without times runs from 00:00 to 23:59, the next reservation of the same vehicle can start on the day after the previous one ends, not on the same day.

Saving

  • The “PRIHVATI REZERVACIJU” button in the sticky footer saves the reservation.
  • Mandatory fields and the rules above are validated before saving.
  • After saving, the user returns to the calendar (in the month it showed), a success toast is shown, and the reservation is shown as a new blue bar on the vehicle’s row for the reserved dates.
Conflict for the assignee to check: the spec calls the button “Spremi rezervaciju”, but Figma shows “PRIHVATI REZERVACIJU”. These docs follow Figma.

4.2 Kalendar zauzetosti

Monthly calendar of all inventory vehicles, including archived ones (grouped into Vozni park by Namen, and Događaji). It shows blue bars for Vozni park reservations (labeled with Tvrtka and/or Ime i prezime), green bars for events (event name) and grey "Neispravno" from today on for vehicles with that status, based on reservations only, not on pickup/return records. It has "+ NOVA REZERVACIJA", a brand filter, month navigation, a vehicle info tooltip (Šasija, Boja), an empty-cell click that opens a prefilled new reservation, and a reservation menu (Uredi rezervaciju / Otvori zapisnik o preuzimanju / Ukloni rezervaciju) with a delete confirmation modal.

Users with access to Vozni park open the calendar from the side menu: Vozni park → Kalendar zauzetosti. It is the start page of the module.

Page title: “Kalendar zauzetosti”. Breadcrumb: Kalendar zauzetosti.

The calendar shows the booking status of all vehicles from the general inventory (Inventar), whichever area they are designated for. Both Vozni park vehicles and Događaji (Events) vehicles appear on the same calendar.

Archived vehicles are also shown, so that their past reservations stay visible. They cannot be selected for new reservations.

Not specified: how archived vehicles are shown. Recommended: only in months where they have a reservation or an event, with the plate in grey.
  • “+ NOVA REZERVACIJA” button – opens Adding new reservation with Vozilo and dates empty
  • Brand filter dropdown, default “SVE MARKE” – filters the calendar by brand
  • Month navigation: “‹ Prosinac 2025 ›” – the arrows go to the previous and next month
The brand filter (“SVE MARKE”) at the top of the page.
Not specified: which month is shown when the page opens. Recommended: the current month.

Calendar grid

  • First column: vehicles, with the heading “VOZNI PARK”.
  • One column per day of the month, labeled with the day number and the weekday (PON, UTO, SRI, ČET, PET, SUB, NED). Saturday and Sunday columns are shaded.
  • The grid scrolls horizontally when the month does not fit the screen.
Conflict for the assignee to check: Figma has two headings for the first column: “VOZNI PARK” (main calendar frame) and “VOZILO” (delete modal frame). These docs follow the main frame.

Vehicle groups

Vehicles are grouped in two sections. The section is set when the vehicle is added to the inventory (Vrsta korištenja):

  • Vozni park – further grouped by purpose (Namen), e.g. IT, Postprodaja, Novinarji, Veliki kupci. Each group header shows the group name and the number of vehicles (e.g. “2 vozila”).
  • Događaji – one group, with the header “DOGAĐAJI”.
Grouping the Vozni park vehicles by Namen, with a vehicle count per group.

The vehicle count uses the correct Croatian form: “1 vozilo”, “2 vozila” … “4 vozila”, “5 vozila”, “21 vozilo”, “22 vozila”. (Figma has the typo “1 vozila”.)

Figma mistakes: the delete modal frame groups vehicles by brand (Audi, Seat, Cupra, VW…), and the main calendar frame repeats two Škoda vehicles in both VELIKI KUPCI and DOGAĐAJI. These docs follow the spec: groups by Namen, and each vehicle appears once. In Figma the Događaji group has no vehicle count. Recommended: show it there too.

Each vehicle row is labeled with brand and model, and the registration plate in grey underneath (e.g. “AUDI A4 Avant” / “ZG-123-AA”).

Bars on the timeline

  • Blue bar – a Vozni park reservation, spanning the reserved days. The label is the company name, the person’s name, or both (e.g. “Jutranji list”, “Ana Horvat”, “Jutranji list – Ana Horvat”).
  • Green bar – a period in which the vehicle is added to an event, labeled with the event name (e.g. “Event test drive - new model”).
  • Grey bar “Neispravno” – based on the vehicle’s current status in the inventory. If the status is Neispravno, the vehicle is shown as Neispravno from today for all future dates. When the status is changed back to Aktivno, the grey bar is no longer shown.
Not specified: are past Neispravno periods shown? The spec talks about “periods” of Neispravno, but only says the vehicle is shown as Neispravno for all future dates, and the status has no history. Recommended: no; once the vehicle is Aktivno again, the past Neispravno period is not shown.

A vehicle marked out of order for a set period with a reservation (e.g. “Odjava”) is shown as a normal blue reservation bar.

Figma mistake: the main calendar frame has the annotation “Car is set as out of order in Vehicles list” on a blue “ODJAVA” bar. Per the spec, “Neispravno” from the inventory is grey, and “Odjava” is an ordinary blue reservation. The Figma frame also fills the whole month with “Neispravno”, including past days.
Not specified: which dates a green event bar spans. Recommended: the event’s start to end date.

Reservations and events of the same vehicle never overlap: a new or changed reservation is rejected if it overlaps another reservation or event of the vehicle (see Adding new reservation).

The grey Neispravno bar doesn’t overlap them either: a vehicle with current or future reservations can’t be set to Neispravno (see Add new vehicle), and a Neispravno vehicle can’t be reserved.

The calendar shows only the reservations. Dates from pickup/return records are not shown. Reservation times and pickup/return times are not connected and can differ. For an accurate calendar, the Manager of Vozni park changes the reservation dates by hand when the actual pickup or return differs from the reservation.

Vehicle info

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2056-8872

Hovering over a vehicle cell shows an info icon (ⓘ). Clicking it opens a tooltip with additional vehicle info:

  • “Šasija: …” – chassis number
  • “Boja: …” – color
Conflict for the assignee to check: the spec says hovering over a vehicle shows the tooltip. These docs follow Figma: hovering shows the info icon, and clicking it opens the tooltip.

Figma note: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2057-11915

There is no option to remove a vehicle on the calendar. (The Figma note and the delete modal frame show a three-dots menu with “Ukloni vozilo” on the vehicle cell. That is a Figma mistake: the three-dots menu belongs to the reservation bar, and its remove option is “Ukloni rezervaciju”.)

Adding a reservation from the calendar

Clicking an empty cell opens Adding new reservation with Vozilo prefilled from the row and Od from the day that was clicked.

Reservation menu

Figma note: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2314-12509

Hovering over a reservation bar shows a three-dots icon ("…"). Clicking an existing reservation opens a menu with:

  • Uredi rezervaciju – opens Editing existing reservation. Past reservations can also be edited.
  • Otvori zapisnik o preuzimanju – starts a new Zapisnik o preuzimanju, with Registarska oznaka, Tvrtka, Ime i prezime, E-mail or Telefon, and Rok korištenja (Od / Do) copied from the reservation. The record is not linked to the reservation.
  • Ukloni rezervaciju (red, with a trash icon) – opens the delete confirmation modal

The menu is shown only for blue reservation bars, not for event or Neispravno bars.

Conflict for the assignee to check: the spec says this option opens the reservation’s own handover protocol (“its handover protocol”), and that the Zapisnici list shows protocols “tied to Vozni park reservations”. These docs (and the DB schema) don’t link records to reservations: each click starts a new record with the reservation data copied in, so clicking twice on the same reservation creates two records.

Delete confirmation modal

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2059-15950

The modal is the same as the other delete modals in the app:

  • Red warning icon, title “Ukloni rezervaciju?”, with a subtitle of the reservation’s name, vehicle and registration plate (e.g. “Ivan Horvat · SEAT Ibiza Style · RI-321-DD”)

  • Text: “Jeste li sigurni da želite ukloniti ovu rezervaciju?”

  • Buttons:

    • Odustani – closes the modal without deleting
    • Ukloni (red) – deletes the reservation and removes its bar from the calendar
  • Close icon (x) in the top right corner – closes the modal without deleting

(The Figma modal subtitle mixes two vehicles: “Ivan Horvat · Seat Ibiza Style VZ · OS-654-EE”, where the plate and “VZ” belong to the Cupra Formentor.)

Deleting a reservation does not affect the Zapisnici: records are not linked to reservations.

4.3 Editing existing reservation

The "Uredi rezervaciju" page, opened from "Uredi rezervaciju" in the reservation menu on the Kalendar zauzetosti. Same fields and validation as Adding new reservation (Vozilo, Od, Do mandatory; Tvrtka or Ime i prezime required; Email / Telefon optional; no overlap with other reservations or events of the vehicle; Neispravno vehicles not available), prefilled. The availability check ignores the reservation itself. Past reservations can be edited too. Saving returns to the calendar with a success toast and the updated bar. Changes do not affect Zapisnici.

Users with access to Vozni park edit a reservation by clicking the reservation bar on the Kalendar zauzetosti and choosing “Uredi rezervaciju”.

Past reservations can also be edited. This is how the Manager of Vozni park keeps the calendar accurate when the actual pickup or return differs from the reservation.

The page has the same fields and validation as Adding new reservation, prefilled with the current data. The availability check (“Vozilo nije dostupno u odabranom razdoblju.”) ignores the reservation that is being edited, so it only fails on overlaps with other reservations or events of the vehicle, or on Neispravno. Page title: “Uredi rezervaciju”, with a back arrow that returns to the calendar. Breadcrumb: Kalendar zauzetosti.

Conflict for the assignee to check: the spec says the page title is “Rezervacija”, but Figma shows “Uredi rezervaciju”. These docs follow Figma.

Changing a reservation does not change any Zapisnik started from it: records are not linked to reservations.

Saving

  • The “PRIHVATI REZERVACIJU” button in the sticky footer saves the changes.
  • After saving, the user returns to the calendar (in the month it showed), a success toast is shown, and the bar is shown at the new dates (or on the new vehicle’s row).
Conflict for the assignee to check: the spec calls the button “Spremi rezervaciju”, but Figma shows “PRIHVATI REZERVACIJU”. These docs follow Figma. The Figma annotation still says “When the reservation is successfully added…”. The toast should say the reservation was updated.

Deleting

The edit page has no delete button. A reservation is deleted from the reservation menu on the calendar (“Ukloni rezervaciju”), with a confirmation modal. See Kalendar zauzetosti.

4.4 Zapisnik o preuzimanju (pickup form)

Two-step vehicle pickup record, started from a reservation on the calendar (data copied, not linked), with "+ NOVI ZAPISNIK O PREUZIMANJU" on the Zapisnici list (empty), or continued from a Draft row. Step 1 has Podaci o vozilu (Registarska oznaka, auto-filled Marka/Tip/Broj šasije/Boja), Oprema vozila, Podaci o korisniku (10 optional fields), Rok korištenja, Stanje vozila pri preuzimanju and Fotografije; it can be saved as Draft at any time. Step 2 shows everything read-only, with the Opći uvjeti checkbox and the user's and Porsche Croatia's signatures. "KREIRAJ ZAPISNIK" sets the status to Preuzeto, makes the pickup PDF and e-mails it with the Opći uvjeti to the user.

Users with access to Vozni park open the pickup record in one of these ways:

  • “Otvori zapisnik o preuzimanju” in the reservation menu on the Kalendar zauzetosti – starts a new record with data copied from the reservation. The record is not linked to the reservation.
  • “+ NOVI ZAPISNIK O PREUZIMANJU” on the Zapisnici list – starts an empty record.
  • “Otvori zapisnik o preuzimanju” on a Draft row of the Zapisnici list – continues the saved draft.

Breadcrumb: Zapisnici > Zapisnik o preuzimanju. Page title: “Zapisnik o preuzimanju — " followed by the vehicle’s Tip and Registarska oznaka (e.g. “Zapisnik o preuzimanju — A4 Avant ZG-123-AA”), with a back arrow that returns to the previous view.

(Figma shows the plate “ZG-123-A1” in the field but “ZG-123-AA” in the title.)

Not specified: the page title before a vehicle is selected. Recommended: “Zapisnik o preuzimanju” only.
Not specified: what happens when the user leaves the page with unsaved changes. Recommended: ask for confirmation before leaving.

The form has two steps.

Step 1

Podaci o vozilu

  • Registarska oznaka – searchable dropdown, mandatory. Prefilled if the record is started from a reservation, otherwise selected by the user. Archived vehicles are not shown.
  • Marka – read-only, filled in from the selected registration plate
  • Tip – read-only, filled in from the selected registration plate
  • Broj šasije – read-only, filled in from the selected registration plate
  • Boja – read-only, filled in from the selected registration plate
  • Info box: “Vozilo je osigurano za slučaj krađe samo na području RH i EU. Putovanje u inozemstvo dopušteno je isključivo uz pismeno odobrenje vlasnika vozila.”

The vehicle data is not copied into the record. It is always shown from the inventory, so later changes in the inventory (e.g. Boja) also show on existing records. The signed PDF keeps the values from the time it was created.

Not specified: can a pickup record be started for a vehicle that is Neispravno, or that already has a Draft or Preuzeto record? Recommended: Neispravno vehicles can’t be selected (as for reservations). Several open records per vehicle are allowed.

Oprema vozila

  • Two prefilled, disabled fields shown by default: “Vatrogasni aparat” and “Komplet obvezne opreme”
  • “+ Add Field” button – adds an empty field (placeholder “Unesite naziv opreme…”) for more equipment
Conflict for the assignee to check: “+ Add Field” (Figma) is the only English label in the form; other sections use “+ Dodaj polje”. Not specified: can an added equipment field be removed? Recommended: yes, with a “–” button, as on the BTC candidate rows.

Podaci o korisniku

All fields are optional.

  • Tvrtka – text (placeholder “Naziv tvrtke”). Copied from the reservation.
  • Adresa tvrtke – text (placeholder “Ulica i broj, grad”)
  • Ime i prezime – text (placeholder “Unesite ime i prezime”). Copied from the reservation.
  • Adresa korisnika – text (placeholder “Ulica i broj, grad”)
  • E-mail – text (placeholder ime@gmail.com). Copied from the reservation’s Email / Telefon if it is an e-mail address. The copy of the record is sent to this address.
  • Telefon – text (placeholder “+385 …”). Copied from the reservation’s Email / Telefon if it is not an e-mail address.
  • Broj vozačke dozvole – text (placeholder “00000000”)
  • Kat. – text (placeholder “npr. B”). Driver’s license category.
  • Izdana od PU – text (placeholder “Policijska uprava”)
  • Vrijedi do – date picker (placeholder “Unesite datum”). Driver’s license expiry date.
Not specified: validation of E-mail (format) and Vrijedi do (an expired license). Recommended: check the e-mail format; show a warning, not an error, when Vrijedi do is before the pickup date.

Rok korištenja

  • Od – date picker, mandatory. Copied from the reservation.
  • Do – date picker, mandatory. Copied from the reservation.
  • Vozilo treba vratiti najkasnije do – time, mandatory (shown with the suffix “h”, e.g. “14 h”)
  • Mjesto povratka – text, optional
  • Poseban dogovor – text, optional
  • Info box (always the same text): “Radno vrijeme: 8,30 - 16,00 (pon. - čet.) 8,30 - 13,30 (petak)”
Conflict for the assignee to check: the spec says “Vozilo treba vratiti najkasnije do” is a time. Figma shows a whole-hour field with the suffix “h”. These docs follow Figma. Note that the office closes at 13,30 on Fridays, which a whole-hour field can’t express.
Conflict for the assignee to check: Mjesto povratka is optional on the pickup screens and in the spec, but marked mandatory (“Mjesto povratka *”) on the return screens, where it is read-only. These docs follow the pickup screens: optional.

Stanje vozila pri preuzimanju

  • Km stanje – number, mandatory (placeholder “npr. 45200”, suffix “km”)
  • Datum preuzimanja – date picker, mandatory. Prefilled from Od in Rok korištenja.
  • Vrijeme preuzimanja – time picker, mandatory
  • Stanje napunjenosti baterije – number, optional (suffix “%”)
  • Razina goriva u spremniku – text, optional
  • Oštećenja evidentirana kod predaje vozila – textarea, optional (placeholder “Opišite vidljiva oštećenja vozila…”)
Conflict for the assignee to check: the fuel field is “Razina goriva u spremniku” on pickup step 1 and the return screens, but “Količina goriva u spremniku” in the spec and on pickup step 2. These docs follow pickup step 1.
Not specified: the range of Stanje napunjenosti baterije. Recommended: a whole number from 0 to 100.

Fotografije

  • “Odaberi datoteke” – upload photos of the vehicle, optional

Upload rules:

  • File types: JPG, PNG, HEIC
  • Up to 10 MB per file
  • Up to 20 photos per record
  • Several files can be selected at once
  • Uploaded photos are listed with their file name and can be removed until the record is created (“KREIRAJ ZAPISNIK”)
Not specified: the upload rules. The spec only says photos are optional. The file types, size and count above are recommended values for the client to confirm.

Step 1 bottom bar (sticky)

  • “PRIHVATI DRAFT” – saves the record as Draft, even if mandatory fields are not filled in, and returns the user to the previous view. The record can be opened again from the Zapisnici list and edited.
  • “POSTAVI POTPISE” – validates the mandatory fields and goes to step 2
Conflict for the assignee to check: the spec and the Figma annotation call the draft button “Spremi draft”, but the Figma button says “PRIHVATI DRAFT”. The spec calls the step 2 button “Nastavi na potpise”, which is also its label on the return screen, but the pickup screen says “POSTAVI POTPISE”. These docs follow the Figma buttons.
Not specified: which fields a draft needs. The Zapisnici list shows Vozilo, Korisnik and Razdoblje for every row. Recommended: Registarska oznaka is required for a draft; everything else can be empty.

Step 2

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2065-21827

All data from step 1 is shown read-only for review (Podaci o vozilu with the insurance info box, Oprema vozila, Podaci o korisniku, Rok korištenja with the working hours box, Stanje vozila pri preuzimanju, Fotografije). Empty optional fields are shown with an empty value.

Not specified: how the user goes back from step 2 to step 1 to correct data. Recommended: the back arrow on step 2 returns to step 1, with the entered data kept.

Potpisi

  • Checkbox, mandatory: “Pročitao sam i prihvatio opće uvjete otisnute na poleđini te izjavljujem da sam suglasan da su oni sastavni dio zapisnika.” The word “zapisnika” links to the Opći uvjeti text.
  • Potpis korisnika pri preuzimanju vozila – signature pad, mandatory (hint “Kliknite i povucite za potpis”)
  • Potpis Porsche Croatia pri preuzimanju vozila – signature pad, mandatory (hint “Kliknite i povucite za potpis”)
The checkbox has no asterisk in Figma, but the spec says the user must accept the terms, so it is mandatory here. The text “otisnute na poleđini” (printed on the back) comes from the paper form; the client should confirm the wording. Not specified: can a signature be cleared and redrawn? Recommended: yes, with a “Obriši” link under each pad.

Info box “Zaštita osobnih podataka”:

“Osobne podatke obrađujemo isključivo u skladu s važećim propisima o zaštiti podataka. Vaši osobni podaci (ime, prezime, adresa, broj vozačke dozvole, broj telefona) navedeni u ovom obrascu nužni su za ispunjenje zakonskih obveza nastalih uslijed/za slučaj prometnih ili komunalnih prekršaja. Ukoliko nastupi prometni/komunalni prekršaj dodatno ćemo od Vas zatražiti i druge osobne podatke (OIB, prebivalište, datum rođenja). Svi navedeni podaci će se obrađivati samo u navedene svrhe i prosljeđivati nadležnim institucijama.”

(Figma has the typos “propisima i zaštiti podataka”, “ime ,prezime” and “adresa ,broj”.)

Step 2 bottom bar (sticky)

  • Text: “Zapisnik će biti pohranjen i kopija će biti automatski poslana na e-mail adresu korisnika.”
  • “KREIRAJ ZAPISNIK” – checks the checkbox and both signatures and creates the record

Creating the record:

  • Sets the record status to Preuzeto on the Zapisnici list. The pickup record can no longer be edited or deleted.
  • Generates the pickup PDF. It can be downloaded from the Zapisnici list.
  • Sends a copy of the pickup record and the Opći uvjeti to the E-mail entered under Podaci o korisniku. If no e-mail was entered, no copy is sent.
  • Returns the user to the previous view, with a success toast.
The bottom bar text says a copy “will be sent” even when no e-mail was entered. Recommended: hide the sentence about the copy when E-mail is empty.
Yet to be specified: the layout of the pickup PDF, and the subject and text of the e-mail. Until then, the PDF should contain the same sections as the view page, with both signatures.

Opći uvjeti

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2065-22259

The general terms are linked from the checkbox in step 2 (shown on their own URL, per the Figma annotation) and sent with the pickup e-mail.

In Figma this text is a screenshot of the paper form, so it was transcribed here. It has to be proofread by the client before it goes live. The text still refers to the paper form (“na prvoj strani”, “otisnute na poleđini”).

OPĆI UVJETI KORIŠTENJA VOZILA IZ VOZNOG PARKA PORSCHE CROATIA d.o.o.

1. Općenito

Korisnik mora vozilo prije preuzimanja detaljno pregledati i provjeriti potpunost opreme kao i dokumentacije vozila. Vozilo osim oštećenja već navedenih na prvoj strani nema drugih oštećenja. Korisnik je detaljno upoznat sa načinom rukovanja opremom vozila.

2. Korištenje vozila

Zabranjeni su:

  • davanje vozila na korištenje trećim osobama
  • prepuštanje vozila trećim osobama,
  • omogućavanje osobama koje nisu navedene u ovom Zapisniku da upravljaju vozilom (posebice predajom ključeva vozila),
  • transport osoba,
  • obuka vozača,
  • vuča ili guranje drugih vozila,
  • pušenje u vozilu,

Korisnik smije vozilo koristiti samo dok ima za to valjanu vozačku dozvolu.

Svojevoljno davanje na popravak od strane korisnika je nedozvoljeno. Kod vozila koja stoje moraju se ispravno osigurati odn. zaključati vrata i prozori. Dokumentacija vozila ne smije se ostavljati u nenadziranom vozilu. Upravljač mora biti zakočen.

Radnje suprotne ovoj odredbi za posljedicu imaju gubitak pokrića osiguranja.

3. Vraćanje vozila

Korisnik je dužan vozilo vratiti u ispravnom stanju u dogovorenom roku na dogovorenom mjestu (u načelu Porsche Croatia, Zagreb - poseban dogovor sa rukovoditeljem marke moguć). Korisnik je obvezan vrijednost nevraćenih sastavnih dijelova, alata i sl. platiti pri vraćanju vozila. To vrijedi i za troškove ponovnog pribavljanja eventualno izgubljene dokumentacije vozila. Isključeno je jamstvo Porsche Croatia za predmete koje korisnik ostavi u vozilu.

4. Osiguranje

Vozilo je kasko osigurano. Korisnik uzima na znanje da osim slučajeva navedenih u točki 2. ne postoji zaštita osiguranja niti ako se oštećenje prouzroči grubom nepažnjom ili namjerno, ili ako je do nezgode ili oštećenja vozila došlo zbog upravljanja vozilom pod utjecajem alkohola ili droga, kao i kod oštećenja, koja nastanu zbog tereta koji se eventualno prevozi.

5. Zaštita Porsche Croatia

Korisnik jamči za sve štete koje nastaju za vrijeme korištenja vozila na vozilu, kao i za štete koje on korištenjem vozila prouzroči trećim osobama. Korisnik se obvezuje, da će Porsche Croatia zaštititi glede odgovornosti za štetu ili tužbe zbog nedostataka koji iz toga nastanu.

Korisnik posebice jamči i za:

  • štete koje nastanu zbog neprimjerenog korištenja vozila,
  • sve posljedice prekršaja zakonskih zabrana ili drugih nezakonitih radnji u zemlji i inozemstvu,
  • naknadu šteta, za koje iz bilo kojih razloga nema jamstvenog pokrića,
  • sve štete koje prouzroči treća osoba, kojoj je prepustio vozilo.

6. Ponašanje u slučaju prometne nezgode

Pri nastanku šteta ili pri sudjelovanju vozila u prometnoj nezgodi odmah telefonski treba informirati Porsche Croatia, Vozni park:

tel. 01 6269 120, 6269 143 mob.: 099 801 4081

Korisnik je obvezan u slučaju sudjelovanja u prometnoj nezgodi poduzeti sve što je za rješenje problema potrebno, posebice odmah obavijestiti policiju, utvrditi reg. oznake drugih vozila uključenih u prometnu nezgodu, utvrditi imena i adrese osoba-sudionika i svjedoka, popuniti europsko izvješće itd. Korisnik je obvezan Porsche Croatia i njegovom osiguratelju bez odlaganja, u svakom slučaju odmah po upitu dati sve informacije koje zatraže. Korisnik nema pravo priznavati ili djelomično priznati niti isplaćivati zahtjeve trećih osoba.

(The source has the typos “uča” (vuča) in point 2 and “štete koji nastaju” (koje) in point 5.)

4.5 Zapisnici list

The "Zapisnici o primopredaji vozila" list of all pickup/return records, with columns Vozilo, Korisnik, Razdoblje (the record's Rok korištenja), Status (Draft / Preuzeto / Vraćeno), Zapisnik o preuzimanju and Zapisnik o povratku. Depending on the status, the record columns open the pickup or return form, open the read-only view, or download the PDF. It has search by vehicle or user, a "+ NOVI ZAPISNIK O PREUZIMANJU" button, pagination, and "Ukloni zapisnik" for Draft records only, with the standard delete confirmation modal.

Users with access to Vozni park open the list from the side menu: Vozni park → Zapisnici. It lists all vehicle pickup/return records.

Conflict for the assignee to check: the spec says the list shows protocols “tied to Vozni park reservations”. These docs don’t link records to reservations (see Kalendar zauzetosti).

Page title: “Zapisnici o primopredaji vozila”. Breadcrumb: Zapisnici.

  • “+ NOVI ZAPISNIK O PREUZIMANJU” button – opens an empty Zapisnik o preuzimanju
  • Search field “Pretraži po vozilu ili korisniku” – searches by vehicle (brand, model, registration plate) and by user (Ime i prezime, Tvrtka)

List columns

  • Vozilo – brand and model, and registration plate (e.g. “Audi A4 Avant, ZG-123-AA”)
  • Korisnik – Ime i prezime and Tvrtka (e.g. “Ivana Horvat, Agencija T d.o.o.”)
  • Razdoblje – Od - Do from the record’s Rok korištenja (e.g. “10.12.2026 - 14.12.2026”). When the record was started from a reservation, these are the reservation dates copied into the record.
  • Status – tag: Draft, Preuzeto or Vraćeno
  • Zapisnik o preuzimanju – buttons, depending on the status
  • Zapisnik o povratku – buttons, depending on the status
  • Actions menu (three dots icon “…”) – Draft records only
Conflict for the assignee to check: the column is “Zapisnik o povratku” (Figma), but the button, the page title and the breadcrumb say “Zapisnik o vraćanju”.

Statuses and actions

StatusZapisnik o preuzimanjuZapisnik o povratku
Draft“Otvori zapisnik o preuzimanju” (form)empty
Preuzeto“Otvori zapisnik o preuzimanju” (view), “Download PDF”“Otvori zapisnik o vraćanju” (form)
Vraćeno“Otvori zapisnik o preuzimanju” (view), “Download PDF”“Otvori zapisnik o vraćanju” (view), “Download PDF”
  • Draft – the pickup record was started but not finalized. “Otvori zapisnik o preuzimanju” opens the pickup form to continue it. The return column is empty, because the vehicle has not been picked up yet.
  • Preuzeto – the pickup record is completed (the vehicle is picked up). “Otvori zapisnik o preuzimanju” opens the Zapisnik o preuzimanju – view, and the pickup PDF can be downloaded. “Otvori zapisnik o vraćanju” opens the return form, which is filled in once the vehicle comes back. A saved return draft is opened with the same button.
  • Vraćeno – both records are completed. Each column opens its view (pickup / return) and downloads its PDF.

There are two PDFs per record: the pickup PDF (Zapisnik o preuzimanju) and the return PDF (Zapisnik o vraćanju).

Conflict for the client to decide: the spec (“Both columns show ‘Download PDF’ only”) and Figma show only “Download PDF” for completed records, with no button to open the view. These docs add “Otvori zapisnik o preuzimanju” / “Otvori zapisnik o vraćanju” next to it to open the view pages.

Status tag colors in Figma: Draft orange, Preuzeto red, Vraćeno grey.

Figma uses the red error color for Preuzeto, which reads as a problem. Recommended: a neutral or positive color, e.g. blue. “Download PDF” (Figma) is in English; the rest of the app is in Croatian.

Sorting and pagination

The list is paginated ("‹ 1 2 … 8 ›").

Not specified: page size and sorting. Recommended: 10 records per page, sorted by Razdoblje Od, newest first.

Deleting a record

Only Draft records can be deleted. Preuzeto and Vraćeno records cannot be deleted.

Clicking the three dots icon on a Draft row opens a menu with “Ukloni zapisnik” (red, with a trash icon), which opens a confirmation modal.

Figma (the same modal, for a reservation): https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2059-15950

The modal is the same as the other delete modals in the app:

  • Red warning icon, title “Ukloni zapisnik?”, with a subtitle of the vehicle, registration plate and user (e.g. “Audi A4 Avant · ZG-123-AA · Ivana Horvat”)
  • Text: “Jeste li sigurni da želite ukloniti ovaj zapisnik?”
  • Buttons:
    • Odustani – closes the modal without deleting
    • Ukloni (red) – deletes the record
  • Close icon (x) in the top right corner – closes the modal without deleting
Not specified: can a saved return draft be discarded? The record stays Preuzeto, and the list has no delete action for Preuzeto. Recommended: no; the return draft is simply overwritten when the return is completed.

4.6 Zapisnik o preuzimanju – view

Read-only view of a submitted pickup record (status Preuzeto or Vraćeno), opened with "Otvori zapisnik o preuzimanju" on the Zapisnici list. It shows the same sections as pickup step 2 (Podaci o vozilu, Oprema vozila, Podaci o korisniku, Rok korištenja, Stanje vozila pri preuzimanju, Fotografije), with the accepted Opći uvjeti, both pickup signatures as images, and a Download PDF button. Nothing can be edited.

Users with access to Vozni park open the view of a submitted pickup record (status Preuzeto or Vraćeno) with “Otvori zapisnik o preuzimanju” in the Zapisnik o preuzimanju column of the Zapisnici list.

There is no separate Figma screen for this page. It uses the read-only layout of Zapisnik o preuzimanju step 2, without the inputs for signing and without the “KREIRAJ ZAPISNIK” bar.

Breadcrumb: Zapisnici > Zapisnik o preuzimanju. Page title: “Zapisnik o preuzimanju — " followed by the vehicle’s Tip and Registarska oznaka (e.g. “Zapisnik o preuzimanju — A4 Avant ZG-123-AA”), with a back arrow that returns the user to where they came from.

Content

All values are shown as text:

  • Podaci o vozilu – Registarska oznaka, Marka, Tip, Broj šasije, Boja, and the insurance info box (“Vozilo je osigurano za slučaj krađe samo na području RH i EU. Putovanje u inozemstvo dopušteno je isključivo uz pismeno odobrenje vlasnika vozila.”)
  • Oprema vozila – Vatrogasni aparat, Komplet obvezne opreme, and any added equipment
  • Podaci o korisniku – Tvrtka, Adresa tvrtke, Ime i prezime, Adresa korisnika, E-mail, Telefon, Broj vozačke dozvole, Kat., Izdana od PU, Vrijedi do
  • Rok korištenja – Od, Do, Vozilo treba vratiti najkasnije do, Mjesto povratka, Poseban dogovor, and the working hours info box
  • Stanje vozila pri preuzimanju – Km stanje, Datum preuzimanja, Vrijeme preuzimanja, Stanje napunjenosti baterije, Razina goriva u spremniku, Oštećenja evidentirana kod predaje vozila
  • Fotografije – the uploaded photos; clicking one opens it in full size
  • Potpisi:
    • The accepted terms: “Pročitao sam i prihvatio opće uvjete otisnute na poleđini te izjavljujem da sam suglasan da su oni sastavni dio zapisnika.” shown as checked, with the link to the Opći uvjeti
    • Potpis korisnika pri preuzimanju vozila – the signature image
    • Potpis Porsche Croatia pri preuzimanju vozila – the signature image

Podaci o vozilu are shown from the inventory, so a later change of the vehicle (e.g. Boja) also shows here. All other values are stored in the record and don’t change, also when the reservation the record was started from changes. The signed PDF keeps all values from the time of pickup.

Not specified: should the view show when the record was created and who created it (the logged-in user who signed for Porsche Croatia)? Recommended: show it under the title, e.g. “Kreirano: 12.08.2026. 9:00, Maša”. The user’s records must be kept even if the user is deleted (see User management).

Actions

  • “Download PDF” – downloads the pickup PDF (the same file as on the Zapisnici list)

4.7 Zapisnik o vraćanju (return form)

Two-step vehicle return record, opened with "Otvori zapisnik o vraćanju" on a Preuzeto record of the Zapisnici list. Step 1 shows the pickup data read-only and adds the editable Stanje vozila pri vraćanju (Km stanje prefilled from pickup, Datum and Vrijeme vraćanja, Voženo calculated in days and hours, battery, fuel, damage) and Fotografije; a draft keeps the status Preuzeto. Step 2 shows everything read-only, with the Opći uvjeti checkbox and the user's and Porsche Croatia's return signatures. "KREIRAJ ZAPISNIK" sets the status to Vraćeno, makes the return PDF and e-mails a copy to the user.

Users with access to Vozni park open the return record with “Otvori zapisnik o vraćanju” on a Preuzeto row of the Zapisnici list. It completes the same record as the Zapisnik o preuzimanju.

Breadcrumb: Zapisnici > Zapisnik o vraćanju. Page title: “Zapisnik o vraćanju — " followed by the vehicle’s Tip and Registarska oznaka (e.g. “Zapisnik o vraćanju — A4 Avant ZG-123-AA”), with a back arrow that returns the user to where they came from.

The form has two steps.

Step 1

Pickup data (read-only)

Carried over from the Zapisnik o preuzimanju and shown read-only, with all values as finalized at pickup (Podaci o vozilu are shown from the inventory, see Zapisnik o preuzimanju – view):

  • Podaci o vozilu – Registarska oznaka, Marka, Tip, Broj šasije, Boja, and the insurance info box
  • Oprema vozila
  • Podaci o korisniku
  • Rok korištenja – Od, Do, Vozilo treba vratiti najkasnije do, Mjesto povratka, Poseban dogovor
  • Stanje vozila pri preuzimanju – Km stanje, Datum preuzimanja, Vrijeme preuzimanja, Stanje napunjenosti baterije, Razina goriva u spremniku, Oštećenja evidentirana kod predaje vozila

See Zapisnik o preuzimanju for the fields.

(The return screens mark the read-only “Mjesto povratka *” with an asterisk; it is optional on the pickup form. The return screens also have no “Radno vrijeme” info box in Rok korištenja.)

Not specified: are the pickup photos shown here? Recommended: no; they are on the pickup view.

Stanje vozila pri vraćanju

  • Km stanje – number, mandatory (placeholder “npr. 45200”, suffix “km”). Prefilled with Km stanje from the pickup record.
  • Datum vraćanja – date picker, mandatory
  • Vrijeme vraćanja – time picker, mandatory
  • Voženo – read-only, mandatory, two values: days (“dana”) and hours (“sati”). Calculated automatically from Datum / Vrijeme preuzimanja and Datum / Vrijeme vraćanja.
  • Stanje napunjenosti baterije – number, optional (suffix “%”)
  • Razina goriva u spremniku – text, optional
  • Oštećenja evidentirana kod povrata vozila – textarea, optional (placeholder “Opišite vidljiva oštećenja vozila…”)

(Figma reuses the pickup label “Oštećenja evidentirana kod predaje vozila” for the return damage field, shows “2 dani” instead of “2 dana”, and leaves the hours value without a label.)

Voženo is the time between pickup and return, in whole days plus the remaining hours. The units use the correct Croatian form: “1 dan”, “2 dana”, “5 dana”; “1 sat”, “2 sata”, “5 sati”.

Not specified: rounding of Voženo. Recommended: minutes are rounded up to the next hour.
Not specified: validation. Recommended: Km stanje at return cannot be lower than at pickup, and Datum / Vrijeme vraćanja cannot be before Datum / Vrijeme preuzimanja.

Fotografije

  • “Odaberi datoteke” – upload photos of the vehicle at return, optional. Uploaded files are listed as links (e.g. “Img_23413413432423423424.jpeg”).

The same upload rules as on the pickup form apply.

Step 1 bottom bar (sticky)

  • “PRIHVATI DRAFT” – saves the return data without changing the record status: it stays Preuzeto. The user returns to the previous view. The Zapisnik o vraćanju can be opened again from the Zapisnici list and edited.
  • “NASTAVI NA POTPISE” – validates the mandatory fields and goes to step 2
Conflict for the assignee to check: the spec and the Figma annotation call the draft button “Spremi draft”, but the Figma button says “PRIHVATI DRAFT”. These docs follow the Figma button.

Step 2

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2065-22836

All data is shown read-only for review: the pickup data, the new Stanje vozila pri vraćanju section and the return Fotografije.

Potpisi

  • Checkbox, mandatory: “Pročitao sam i prihvatio opće uvjete otisnute na poleđini te izjavljujem da sam suglasan da su oni sastavni dio zapisnika.” The word “zapisnika” links to the Opći uvjeti.
  • Potpis korisnika pri vraćanju vozila – signature pad, mandatory (hint “Kliknite i povucite za potpis”)
  • Potpis Porsche Croatia pri vraćanju vozila – signature pad, mandatory (hint “Kliknite i povucite za potpis”)
  • Info box “Zaštita osobnih podataka” – the same text as on the pickup form

Step 2 bottom bar (sticky)

  • Text: “Zapisnik će biti spremljen i kopija će biti automatski poslana na e-mail adresu korisnika.”
  • “KREIRAJ ZAPISNIK” – checks the checkbox and both signatures and completes the record

Completing the record:

  • Sets the record status to Vraćeno on the Zapisnici list. The record can no longer be edited or deleted.
  • Generates the return PDF. It can be downloaded from the Zapisnici list.
  • Sends a copy of the return record to the E-mail entered under Podaci o korisniku on the pickup form. If no e-mail was entered, no copy is sent.
  • Returns the user to the previous view, with a success toast.
Conflict for the assignee to check: the spec sends the Opći uvjeti with the pickup copy only, not with the return copy. The bottom bar text says “pohranjen” on the pickup form and “spremljen” here (both from Figma).
Yet to be specified: the layout of the return PDF, and the subject and text of the e-mail. Until then, the PDF should contain the same sections as the view page, with both return signatures.

4.8 Zapisnik o vraćanju – view

Read-only view of a completed return record (status Vraćeno), opened with "Otvori zapisnik o vraćanju" on the Zapisnici list. It shows the same sections as return step 2 (pickup data, Stanje vozila pri vraćanju with Voženo, return Fotografije), with the accepted Opći uvjeti, the return signatures of the user and Porsche Croatia as images, and a Download PDF button. Nothing can be edited.

Users with access to Vozni park open the view of a completed return record (status Vraćeno) with “Otvori zapisnik o vraćanju” in the Zapisnik o povratku column of the Zapisnici list.

There is no separate Figma screen for this page. It uses the read-only layout of Zapisnik o vraćanju step 2, without the inputs for signing and without the “KREIRAJ ZAPISNIK” bar.

Breadcrumb: Zapisnici > Zapisnik o vraćanju. Page title: “Zapisnik o vraćanju — " followed by the vehicle’s Tip and Registarska oznaka (e.g. “Zapisnik o vraćanju — A4 Avant ZG-123-AA”), with a back arrow that returns the user to where they came from.

Content

All values are shown as text, as they were when the record was completed, except Podaci o vozilu, which are shown from the inventory (see Zapisnik o preuzimanju – view):

  • Pickup data – Podaci o vozilu, Oprema vozila, Podaci o korisniku, Rok korištenja and Stanje vozila pri preuzimanju, as on the Zapisnik o preuzimanju – view
  • Stanje vozila pri vraćanju – Km stanje, Datum vraćanja, Vrijeme vraćanja, Voženo (days and hours), Stanje napunjenosti baterije, Razina goriva u spremniku, Oštećenja evidentirana kod povrata vozila
  • Fotografije – the photos uploaded at return; clicking one opens it in full size
  • Potpisi:
    • The accepted terms, shown as checked, with the link to the Opći uvjeti
    • Potpis korisnika pri vraćanju vozila – the signature image
    • Potpis Porsche Croatia pri vraćanju vozila – the signature image

The pickup signatures and pickup photos are on the pickup view.

Actions

  • “Download PDF” – downloads the return PDF (the same file as in the Zapisnik o povratku column on the Zapisnici list)

4.9 Backend: Adding new reservation

Backend task for creating a Vozni park reservation (POST /drivepark/reservations) – check the API spec and DB structure, apply the drive park schema, set up drivepark.access checks, lock the vehicle and check overlaps with other reservations and events, fill the vehicle usage overview, implement, and verify against the spec.

Docs: Adding new reservation Spec: POST /drivepark/reservations (createDriveParkReservation), DriveParkReservationNewRequest, DriveParkReservation, DriveParkReservationConflict in api-docs/openapi.yaml DB: DriveParkReservations, VehicleReservations (DriveParkReservationId) in api/database/pending_schema.sql (-- DRIVE PARK --)

This is the first Vozni park endpoint, so it also includes the shared setup for the section: the schema, the permission check, the vehicle lock and the overlap check.

1. Double check API spec and DB structure

  • Every docs field is in DriveParkReservationNewRequest, with the right required/optional setting: Vozilo (vehicleId, required), Od / Do (startDate / endDate, required), Tvrtka (company), Ime i prezime (fullName), Email / Telefon (contact, one free-text field).
  • Times: the form has dates only. startTime / endTime are optional and default to 00:00 / 23:59, so a reservation covers whole days and the next one can start the day after. Check the defaults match the docs (“starts at 00:00 on Od, ends at 23:59 on Do”).
  • Company or name: at least one of company / fullName → 400 validation_error, and CK_DriveParkReservations_Name in the DB. Treat an empty or whitespace string as null.
  • Availability: the docs show one message (“Vozilo nije dostupno u odabranom razdoblju.”) for three cases. Check that the spec maps them to 409: vehicle_unavailable (vehicle missing / deleted / archived, or Neispravno = IsActive = 0 while the period ends today or later in Europe/Zagreb) and vehicle_conflict (overlap with any other VehicleReservations row of the vehicle, Vozni park or event).
  • Event side: the event endpoints must also see Vozni park bookings as conflicts. Check that their conflict queries include VehicleReservations rows with EventId IS NULL (on main, the conflict query in EventAssociationSync.SyncEventVehiclesAsync and ReconcileVehicleReservationsAsync uses JOIN Events, which drops those rows) and that the error / removedVehicles entry has a sensible name for a reservation (conflictingEventName). The pending events/vehicles locking PR may already cover this.
  • Lock order: the overlap rule can’t be a constraint, so the vehicle’s Vehicles row is locked (UPDLOCK, HOLDLOCK) before the check, before any other write. Check that the order of locks matches the events/vehicles locking PR (ascending vehicle Id), so an event sync and a reservation can’t deadlock.
  • Vehicle dropdown: uses GET /vehicles (getVehicles, search on name/plate/VIN, archived excluded by default). getVehicles applies limitedBrandCodes, but Vozni park shows all brands. Confirm a Manager of Vozni park has no brand restriction.
  • Roles and permissions: drivepark.access exists in Permissions.cs and the PermissionName enum, granted to Admin and DriveManager. No new permission.
  • DB: DriveParkReservations columns vs request fields (lengths 255); CreatedBy, CreatedAt set by the server (same CreatedBy value as Events); CK_VehicleReservations_Owner (exactly one of EventId / DriveParkReservationId); UX_VehicleReservations_DriveParkReservationId.
  • Is 201 + DriveParkReservation enough for the next step (back to the calendar with a toast, new bar)?

2. Implementation

  • DB: apply the -- DRIVE PARK -- section of pending_schema.sql (run the duplicate check before the unique index; keep the rollback). Later these statements move to create_schema.sql, together with the updated VehicleReservations table comment.
  • Roles and permissions: require drivepark.access → 403.
  • Shared helpers (used by the update task too):
    • Lock: SELECT … FROM Vehicles WITH (UPDLOCK, HOLDLOCK) WHERE Id = @VehicleId inside the transaction, before any write; several vehicles → one at a time, ascending Id.
    • Availability: vehicle exists, DeletedAt IS NULL, isArchived = 0; and not (IsActive = 0 and end ≥ today 00:00 Europe/Zagreb) → otherwise 409 vehicle_unavailable.
    • Overlap: vr.VehicleId = @VehicleId AND vr.StartTime < @EndTime AND vr.EndTime > @StartTime, excluding the reservation’s own row on update → 409 vehicle_conflict, with the other booking (event name or reservation label) in message.
  • Function + repository: in one transaction, lock, check, insert DriveParkReservations and its VehicleReservations row (EventId NULL, DriveParkReservationId, StartTime = startDate + (startTime ?? 00:00), EndTime = endDate + (endTime ?? 23:59)).
  • Validation → 400 validation_error: required fields, end after start, company/fullName, lengths.
  • Response 201 with DriveParkReservation (vehicle brand/name/plate, label = company and fullName joined with " – “).
  • Vehicle usage overview: fill drivepark-reservation entries in VehicleRepository.GetVehicleUsageAsync (GET /vehicles/{vehicleId}/usage): name = label, dates from the VehicleReservations row. Update the VehicleUsageEntry record comment.
  • isOccupied / delete / archive: they read VehicleReservations, so Vozni park reservations count automatically. Check the vehicle update/archive/delete paths lock the Vehicles row the same way, so a reservation can’t be added while the vehicle is being set to Neispravno or archived.

3. Verify implementation against spec

  • Route, method, request and response shapes match createDriveParkReservation.
  • Every status code and error value in the spec can be produced (400 validation_error, 409 vehicle_unavailable, 409 vehicle_conflict); no undocumented ones.
  • DriveManager and Admin → 201; BrandManager and Promoter → 403.
  • Overlap cases: same days as another reservation → 409; ending on the day another starts → 409 (both cover that day); ending the day before → 201; overlapping an event’s window → 409.
  • Neispravno vehicle: period in the past → 201; period including today → 409. Archived vehicle → 409.
  • Two parallel requests for the same vehicle and days → exactly one succeeds.
  • Adding the reserved vehicle to an event for the same days → the event endpoint rejects it.
  • The vehicle’s usage overview shows “Vozni park - rezervacija” with the label.

Done when

A Manager of Vozni park can add a reservation; it can’t overlap any other booking of the vehicle, and it shows on the calendar and in the vehicle’s usage overview.

4.10 Backend: Kalendar zauzetosti

Backend task for the Kalendar zauzetosti (GET /drivepark/calendar) – check the API spec and DB structure, return all vehicles (archived only with bookings in the month) with their reservations and event bookings for a month, implement, and verify against the spec.

Docs: Kalendar zauzetosti Spec: GET /drivepark/calendar (getDriveParkCalendar), DriveParkCalendar, DriveParkCalendarVehicle (allOf Vehicle), DriveParkCalendarEntry in api-docs/openapi.yaml DB: Vehicles, VehicleReservations, DriveParkReservations, Events in api/database/pending_schema.sql (-- DRIVE PARK --) and create_schema.sql

1. Double check API spec and DB structure

  • Vehicles shown: all non-deleted vehicles; archived ones only when they have an entry in the month (the docs show archived vehicles so past reservations stay visible). Confirm this with the docs comment (“Recommended: only in months where they have a reservation or an event”).
  • Groups: Vozni park = IsForDrive, grouped by Purpose (frontend values it, after-sales, vip, press); Događaji = IsForEvent. Grouping and counts (“1 vozilo”, “2 vozila”) are done by the frontend. Check every vehicle has exactly one of the two flags, or decide where a vehicle with both / neither goes.
  • Entries: VehicleReservations rows overlapping the month, reservation (blue, label = company – fullName) or event (green, event name). Check past event bookings are kept when an event is archived (only future bookings are released) so past green bars stay.
  • Neispravno: not an entry; the frontend shows grey from today when isActive is false. Check the docs agree (based on the current status, from today on).
  • Brand filter: brandCode (nice-to-have “SVE MARKE”). No limitedBrandCodes restriction: Vozni park shows all brands.
  • Not shown: pickup/return records (Zapisnici).
  • Event dates: green bars span the event’s StartTime–EndTime from its VehicleReservations row. The docs comment (“Recommended: the event’s start to end date”) matches; confirm.
  • Roles and permissions: drivepark.access.
  • DB: IX_VehicleReservations_VehicleId is enough for one month of data. If the query is slow, add an index on (StartTime, EndTime) with a migration.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Validate month (YYYY-MM) and brandCode → 400 validation_error.
  • One query for the vehicles and one for the entries of the month (StartTime < first day of next month AND EndTime > first day of month), LEFT JOIN DriveParkReservations and Events for the label; group entries per vehicle in code. No query per vehicle.
  • Order vehicles: Vozni park (by purpose) first, then Događaji; then brandCode, name, plate. Entries by start.
  • Map to DriveParkCalendarVehicle (Vehicle fields, isOccupied / canVehicleBeDeleted omitted) and DriveParkCalendarEntry (startDate / startTime / endDate / endTime, HH:mm).

3. Verify implementation against spec

  • Route, method, query parameters and response shape match getDriveParkCalendar.
  • 400 (bad month) / 401 / 403 / 500 can be produced; no undocumented status codes.
  • A reservation from 28.11. to 3.12. shows in both November and December.
  • An archived vehicle with a reservation in the month is included; without one it isn’t. A deleted vehicle is never included.
  • A vehicle with IsActive = 0 is returned with isActive: false and its existing entries.
  • brandCode=A returns only Audi vehicles.

Done when

The calendar shows every vehicle of the month with its blue reservation bars, green event bars and Neispravno state.

4.11 Backend: Editing existing reservation

Backend task for loading and updating a Vozni park reservation (GET and POST /drivepark/reservations/{reservationId}) – check the API spec and DB structure, reuse the lock and overlap check while ignoring the reservation's own booking, handle a vehicle change, implement, and verify against the spec.

Docs: Editing existing reservation Spec: GET /drivepark/reservations/{reservationId} (getDriveParkReservation), POST /drivepark/reservations/{reservationId} (updateDriveParkReservation), DriveParkReservationNewRequest, DriveParkReservation, DriveParkReservationNotFound, DriveParkReservationConflict in api-docs/openapi.yaml DB: DriveParkReservations, VehicleReservations in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • The form has the same fields as Adding new reservation, prefilled. Check DriveParkReservation returns everything the form needs (vehicle id, brand, name and plate for the dropdown, dates, company, fullName, contact).
  • Past reservations can be edited (docs). Check the spec has no “past” rule.
  • Overlap check ignores the reservation’s own VehicleReservations row.
  • Archived / deleted vehicle: checked only when vehicleId changes, so a past reservation of a since-archived vehicle can still be edited. Neispravno is always checked (period ending today or later).
  • Vehicle change: both vehicles are locked, ascending Id. Check the lock order against the events/vehicles locking PR.
  • Zapisnici: not linked to reservations; an update changes no record. Check nothing in the records reads reservation data.
  • Labels: Figma CTA “PRIHVATI REZERVACIJU” and title “Uredi rezervaciju” are frontend only; the toast says the reservation was updated.
  • Roles and permissions: drivepark.access on both endpoints.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • GET: load the reservation with its VehicleReservations row and vehicle → 200 DriveParkReservation; unknown id → 404.
  • POST: in one transaction, lock the vehicle(s), run the shared availability and overlap helpers from Backend: Adding new reservation (excluding the own row), then update DriveParkReservations (UpdatedAt) and its VehicleReservations row (VehicleId, StartTime, EndTime).
  • Validation → 400 validation_error, same as create.
  • Response 200 with the updated DriveParkReservation.

3. Verify implementation against spec

  • Routes, methods, request and response shapes match getDriveParkReservation and updateDriveParkReservation.
  • 400 / 404 / 409 vehicle_unavailable / 409 vehicle_conflict can be produced; no undocumented ones.
  • Moving a reservation by one day where it only overlaps itself → 200.
  • Changing the vehicle to one booked on those days → 409; to a free one → 200, and the bar moves to the new row.
  • Editing a past reservation of a vehicle that is now archived (same vehicle) → 200.
  • BrandManager → 403.

Done when

A Manager of Vozni park can change any reservation, including past ones, without creating an overlap.

4.12 Backend: Delete reservation

Backend task for deleting a Vozni park reservation (DELETE /drivepark/reservations/{reservationId}) – confirm hard delete of the reservation and its booking row, implement, and verify against the spec.

Docs: Kalendar zauzetosti (Reservation menu → Ukloni rezervaciju, Delete confirmation modal) Spec: DELETE /drivepark/reservations/{reservationId} (deleteDriveParkReservation), DriveParkReservationNotFound in api-docs/openapi.yaml DB: DriveParkReservations, VehicleReservations in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • Hard delete: the spec and schema delete the DriveParkReservations row and its VehicleReservations row permanently. The reservation then also disappears from the vehicle’s usage overview. Confirm no history of deleted reservations is needed.
  • Past, current and future reservations can be deleted (no rule in the docs). Confirm.
  • Zapisnici are not linked, so a record started from the reservation stays.
  • The modal subtitle (name · vehicle · plate) comes from the calendar entry the frontend already has; no extra data needed.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • In one transaction: delete the VehicleReservations row (DriveParkReservationId), then the DriveParkReservations row. No vehicle lock needed: deleting can’t create an overlap.
  • Unknown id → 404. Success → 204.

3. Verify implementation against spec

  • Status codes match deleteDriveParkReservation.
  • After deleting, the bar is gone from the calendar and a new reservation for the same days → 201.
  • Deleting twice → 404 the second time. BrandManager → 403.

Done when

A Manager of Vozni park can remove a reservation, and the vehicle is free again for that period.

4.13 Backend: Zapisnik o preuzimanju (pickup form)

Backend task for creating and saving the pickup record (POST /drivepark/records, POST /drivepark/records/{recordId}/pickup) – check the API spec and DB structure, save drafts, complete the pickup with signatures, PDF and e-mail, fill the vehicle usage overview, implement, and verify against the spec.

Docs: Zapisnik o preuzimanju (pickup form) Spec: POST /drivepark/records (createDriveParkRecord), POST /drivepark/records/{recordId}/pickup (saveDriveParkRecordPickup), DriveParkRecordPickupRequest, DriveParkRecordSigning, DriveParkRecordDetail, DriveParkRecordConflict in api-docs/openapi.yaml DB: DriveParkRecords, DriveParkRecordEquipment in api/database/pending_schema.sql (-- DRIVE PARK --)

This is the first Zapisnici endpoint, so it also includes the shared setup for records: the blob container, signature storage, PDF generation and the e-mail.

1. Double check API spec and DB structure

  • Every docs field of step 1 is in DriveParkRecordPickupRequest and DriveParkRecords:
    • Podaci o vozilu – Registarska oznaka (vehicleId); Marka, Tip, Broj šasije, Boja are not sent and not stored, they are read from Vehicles.
    • Oprema vozila – equipment (added items only; the 2 fixed items are in code).
    • Podaci o korisniku – 10 optional fields (company … driverLicenseValidUntil).
    • Rok korištenja – usageFrom, usageTo, returnByTime, returnLocation, specialArrangement.
    • Stanje vozila pri preuzimanju – pickupMileageKm, pickupDate, pickupTime, pickupBatteryPercent (0–100), pickupFuelLevel (text), pickupDamage.
  • Mandatory for completion: Od, Do, Vozilo treba vratiti najkasnije do, Km stanje, Datum and Vrijeme preuzimanja, the terms checkbox and both signatures. Check the docs and the spec list the same fields. Mjesto povratka is optional (Figma marks it mandatory on the return screens; docs comment).
  • Draft: only vehicleId is required (docs comment: “Recommended: Registarska oznaka is required for a draft”). Confirm with the project manager.
  • “Vozilo treba vratiti najkasnije do”: stored as TIME(0), API HH:mm. Figma shows a whole-hour field (“14 h”); docs mark it as a conflict for the assignee. Confirm whether minutes are allowed.
  • Neispravno vehicle: the spec only rejects deleted / archived vehicles (vehicle_unavailable). The docs comment recommends Neispravno vehicles can’t be selected. Decide and update the spec.
  • Several open records per vehicle are allowed. Confirm.
  • Validation not decided in the docs: e-mail format (the spec has format: email), Vrijedi do before the pickup date (docs recommend a warning only). Confirm.
  • Prefill from a reservation: done by the frontend from getDriveParkReservation (contact goes to E-mail if it contains “@”, otherwise Telefon). The record keeps no link to the reservation.
  • PDF and e-mail: the PDF layout and the e-mail subject/text are not specified yet (docs: “Yet to be specified”). The Opći uvjeti text is in the docs (transcribed, to be proofread); decide how it is attached (a static PDF file in the API, or rendered).
  • E-mail status: PickupEmailStatus records pending / sent / failed; there is no retry. Confirm whether a retry (e.g. like EventInviteeEmailTimerFunction) is needed.
  • Signatures: PNG data URLs in the request, stored as blobs, returned as data URLs. Decide a size limit (→ 400).
  • Vehicle “Zauzeto”: the inventory docs say a vehicle is Zauzeto when it is “reserved or picked up in Vozni park”, but isOccupied only reads VehicleReservations. A Preuzeto record without a reservation (records aren’t linked to reservations) doesn’t make the vehicle Zauzeto. Decide whether a Preuzeto record should count.
  • Roles and permissions: drivepark.access.
  • Is the DriveParkRecordDetail response enough for the next step (back to the previous view with a toast; the frontend keeps the record id for photos)?

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Blob storage: a drivepark container via BlobStorageService for signatures, PDFs and photos; blob names per record (e.g. records/{recordId}/…).
  • createDriveParkRecord: check the vehicle (exists, not deleted, not archived → 409 vehicle_unavailable), insert the record and the equipment rows (OrderIndex) in one transaction. With signing, complete it as below.
  • saveDriveParkRecordPickup: record must be Draft (PickupCompletedAt IS NULL) → else 409 record_not_draft. Put the check in the UPDATE’s WHERE and handle 0 rows (a parallel completion). Replace all pickup fields, replace the equipment rows (one multi-row insert).
  • Completion (with signing): validate the mandatory fields → 400 validation_error with the missing fields in message; upload both signatures; generate the PDF (reuse the approach of PdfTemplateService); set PickupCompletedAt, PickupCompletedBy, the blob columns and PickupEmailStatus (pending, or NULL without an e-mail). After the commit, send the PDF and the Opći uvjeti with BrevoEmailService and set sent / failed. A failed e-mail doesn’t fail the request. If the transaction fails, delete the uploaded blobs.
  • Validation → 400 validation_error: usageTo ≥ usageFrom, battery 0–100, mileage ≥ 0, lengths, HH:mm.
  • Response 201 / 200 with DriveParkRecordDetail (computed status).
  • Vehicle usage overview: fill drivepark-rental (“Vozni park - aktivni najam”) entries in GetVehicleUsageAsync from completed pickups: name = fullName or company, from PickupDate to ReturnDate (or UsageTo while not returned).
  • isOccupied (“Zauzeto”): if confirmed in step 1, add “has a Preuzeto record” to IsOccupiedByFutureReservationSql.

3. Verify implementation against spec

  • Routes, methods, request and response shapes match createDriveParkRecord and saveDriveParkRecordPickup.
  • 400 validation_error, 404, 409 record_not_draft, 409 vehicle_unavailable can be produced; no undocumented ones.
  • Draft with only vehicleId → 201, status draft. Completing without signatures or without Km stanje → 400. Complete → status picked-up, PDF downloadable, e-mail sent.
  • Completing without an e-mail → 200, pickupEmailStatus null, nothing sent.
  • Saving the pickup of a Preuzeto record → 409. Changing the vehicle’s color afterwards shows the new color in getDriveParkRecord but doesn’t change the stored PDF.
  • Two parallel completions of the same draft → one 200, one 409.
  • BrandManager → 403.

Done when

A Manager of Vozni park can fill in the pickup form, save it as a draft, sign it, and the user gets the signed record by e-mail.

4.14 Backend: Zapisnici list

Backend task for the Zapisnici list and the PDF download (GET /drivepark/records, GET /drivepark/records/{recordId}/pdf/{stage}) – check the API spec and DB structure, compute the status, search and sort, stream the stored PDFs, implement, and verify against the spec.

Docs: Zapisnici list Spec: GET /drivepark/records (getDriveParkRecords), GET /drivepark/records/{recordId}/pdf/{stage} (downloadDriveParkRecordPdf), DriveParkRecord, DriveParkRecordStatus, DriveParkRecordStage in api-docs/openapi.yaml DB: DriveParkRecords in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • Every list column has a field in DriveParkRecord: Vozilo (vehicleBrandCode, vehicleName, vehiclePlate), Korisnik (fullName, company), Razdoblje (usageFrom – usageTo, the record’s own Rok korištenja), Status (status).
  • Status is computed (Draft / Preuzeto / Vraćeno from PickupCompletedAt / ReturnCompletedAt), not stored. Check the button table in the docs can be built from status alone: Draft → open pickup form; Preuzeto → pickup view + pickup PDF + open return form; Vraćeno → both views + both PDFs. (The spec and Figma show only “Download PDF” for completed records; the docs add “Otvori zapisnik …” to open the views. The client decides.)
  • Sorting and page size: not specified (docs comment: “Recommended: 10 records per page, sorted by Razdoblje Od, newest first”). The spec sorts by usageFrom descending, nulls first. Confirm; the frontend sends perPage=10.
  • Search: vehicle (name, plate) and user (fullName, company). Check the docs agree (“Pretraži po vozilu ili korisniku”).
  • Two PDFs per record, one per stage. The PDF layout is not specified yet.
  • Roles and permissions: drivepark.access.
  • DB: IX_DriveParkRecords_UsageFrom for the sort.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • List: one count query and one page query (OFFSET / FETCH), joined with Vehicles for vehicleBrandCode, vehicleName, vehiclePlate (no DeletedAt / archived filter); search with LIKE on Vehicles.Name, Vehicles.Plate, FullName and Company; status as a CASE expression.
  • PDF: load the record; the stage’s *PdfBlob is null (stage not completed) → 404; otherwise stream the blob as application/pdf with Content-Disposition (e.g. Zapisnik-o-preuzimanju-ZG-123-AA.pdf).

3. Verify implementation against spec

  • Routes, query parameters and response shapes match getDriveParkRecords and downloadDriveParkRecordPdf.
  • 401 / 403 / 404 / 500 can be produced; no undocumented ones.
  • One record of each status → draft, picked-up, returned.
  • Search by part of the plate and by part of the company name both find the record.
  • Return PDF of a Preuzeto record → 404; pickup PDF of a Draft → 404.

Done when

The Zapisnici list shows every record with its status and the right buttons, and the PDFs can be downloaded.

4.15 Backend: Zapisnik o preuzimanju – view

Backend task for loading a record (GET /drivepark/records/{recordId}) for the pickup form and the pickup view – check the API spec and DB structure, return the vehicle data, equipment, photos and signatures, implement, and verify against the spec.

Docs: Zapisnik o preuzimanju – view Spec: GET /drivepark/records/{recordId} (getDriveParkRecord), DriveParkRecordDetail, DriveParkRecordPhoto, DriveParkRecordNotFound in api-docs/openapi.yaml DB: DriveParkRecords, DriveParkRecordEquipment, DriveParkRecordPhotos in api/database/pending_schema.sql (-- DRIVE PARK --)

The same endpoint also loads a Draft into the pickup form and a record into the return form and return view (Backend: Zapisnik o vraćanju – view).

1. Double check API spec and DB structure

  • Every section of the view is in DriveParkRecordDetail: Podaci o vozilu (vehicle* fields from Vehicles), Oprema vozila (equipment; the 2 fixed items come from the frontend), Podaci o korisniku, Rok korištenja, Stanje vozila pri preuzimanju, Fotografije (photos with stage: pickup), Potpisi (pickupUserSignature, pickupCompanySignature).
  • The vehicle data is not stored in the record: it is read from Vehicles, so later inventory changes show in the view (the signed PDF keeps the old values). Deleted and archived vehicles must still be returned.
  • Created by / when: the docs comment recommends showing “Kreirano: date, user” under the title. pickupCompletedAt / pickupCompletedBy are in the response; confirm with the project manager whether the view shows them.
  • Signatures as data URLs: each is a small PNG. Check the response size is acceptable; otherwise add a signature download endpoint.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Load the record joined with Vehicles (no DeletedAt / archived filter), its equipment (by OrderIndex) and photos (by CreatedAt) — three queries at most, no query per photo. Unknown id → 404.
  • Read the signature blobs and return them as data:image/png;base64,…; null while the stage isn’t completed.
  • Computed fields: status; drivenDays / drivenHours (see the return view task).

3. Verify implementation against spec

  • Route and response shape match getDriveParkRecord.
  • 401 / 403 / 404 / 500 can be produced; no undocumented ones.
  • Draft → signatures null, pickupCompletedAt null. Preuzeto → both pickup signatures set, return fields null.
  • Equipment comes back in the order it was entered.

Done when

A completed pickup record can be opened read-only with all its data, photos and both signatures.

4.16 Backend: Zapisnik o vraćanju (return form)

Backend task for saving and completing the return record (POST /drivepark/records/{recordId}/return) – check the API spec and DB structure, save return drafts without changing the status, complete the return with signatures, PDF and e-mail, implement, and verify against the spec.

Docs: Zapisnik o vraćanju (return form) Spec: POST /drivepark/records/{recordId}/return (saveDriveParkRecordReturn), DriveParkRecordReturnRequest, DriveParkRecordSigning, DriveParkRecordDetail, DriveParkRecordConflict in api-docs/openapi.yaml DB: DriveParkRecords (Return* columns) in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • Every docs field of Stanje vozila pri vraćanju is in DriveParkRecordReturnRequest: returnMileageKm (mandatory, prefilled by the frontend from pickupMileageKm), returnDate, returnTime (mandatory), returnBatteryPercent (0–100), returnFuelLevel, returnDamage. Voženo is computed (drivenDays / drivenHours), not sent.
  • Mandatory for completion: Km stanje, Datum and Vrijeme vraćanja, the terms checkbox and both signatures. Check the docs and the spec agree.
  • Draft: saving keeps the status Preuzeto (docs). Check the spec says the same.
  • Validation not decided in the docs: return km ≥ pickup km, and return date/time not before pickup date/time (docs: “Recommended”). Decide and add to the spec (→ 400).
  • Opći uvjeti with the return e-mail: the spec sends only the return PDF (as the docs' conflict comment notes, the spec attaches the terms to the pickup copy only). Confirm.
  • PDF and e-mail: layout and texts are not specified yet.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Record must be Preuzeto (PickupCompletedAt IS NOT NULL AND ReturnCompletedAt IS NULL) → else 409 record_not_picked_up. Put the check in the UPDATE’s WHERE and handle 0 rows.
  • Replace the return fields only; pickup fields can’t change here.
  • Completion (with signing): validate → 400 validation_error; upload the signatures; generate the return PDF; set ReturnCompletedAt, ReturnCompletedBy, the blob columns and ReturnEmailStatus; after the commit e-mail the PDF to Email and set sent / failed. Reuse the helpers from Backend: Zapisnik o preuzimanju.
  • Response 200 with DriveParkRecordDetail.
  • Vehicle usage overview: the drivepark-rental entry now ends on ReturnDate.

3. Verify implementation against spec

  • Route, request and response shapes match saveDriveParkRecordReturn.
  • 400 validation_error, 404, 409 record_not_picked_up can be produced; no undocumented ones.
  • Return draft → status stays picked-up, values come back on the next load.
  • Complete → status returned, return PDF downloadable, e-mail sent if an e-mail was entered.
  • Return on a Draft or a Vraćeno record → 409.

Done when

A Manager of Vozni park can fill in, save and sign the return, and the record becomes Vraćeno.

4.17 Backend: Zapisnik o vraćanju – view

Backend task for the return part of GET /drivepark/records/{recordId} – check the API spec and DB structure, return the return data, photos and signatures, compute Voženo, implement, and verify against the spec.

Docs: Zapisnik o vraćanju – view Spec: GET /drivepark/records/{recordId} (getDriveParkRecord), DriveParkRecordDetail (return*, drivenDays, drivenHours) in api-docs/openapi.yaml DB: DriveParkRecords (Return* columns), DriveParkRecordPhotos in api/database/pending_schema.sql (-- DRIVE PARK --)

The endpoint itself is built in Backend: Zapisnik o preuzimanju – view. This task covers the return part.

1. Double check API spec and DB structure

  • Every section of the view is in DriveParkRecordDetail: the pickup data, Stanje vozila pri vraćanju (returnMileageKm, returnDate, returnTime, drivenDays / drivenHours, returnBatteryPercent, returnFuelLevel, returnDamage), Fotografije (stage: return), Potpisi (returnUserSignature, returnCompanySignature).
  • Voženo: not specified in the docs (comment: “Recommended: minutes are rounded up to the next hour”). The spec: whole days, then the remaining minutes rounded up to hours, 24 h carry over to a day. Confirm with the project manager.
  • The frontend uses the Croatian forms (“1 dan / 2 dana / 5 dana”, “1 sat / 2 sata / 5 sati”); the API returns numbers only.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Compute drivenDays / drivenHours from PickupDate + PickupTime and ReturnDate + ReturnTime (local wall-clock values, no time zone conversion); null if any of the four is missing. Put it in one helper, also used by the PDF.
  • Return signatures as data URLs, null until the return is completed.

3. Verify implementation against spec

  • Response fields match DriveParkRecordDetail.
  • Voženo: 12.08. 9:00 → 14.08. 20:00 = 2 days 11 hours; 12.08. 9:00 → 12.08. 9:01 = 0 days 1 hour; 12.08. 9:00 → 13.08. 8:30 = 1 day 0 hours (23:30 rounds up to 24 h).
  • Preuzeto record with a return draft → return values set, return signatures null.

Done when

A returned record can be opened read-only with the return data, Voženo, photos and both return signatures.

4.18 Backend: Delete Zapisnik

Backend task for deleting a Draft record (DELETE /drivepark/records/{recordId}) – confirm hard delete, check the Draft-only rule, remove child rows and blobs, implement, and verify against the spec.

Docs: Zapisnici list (Deleting a record) Spec: DELETE /drivepark/records/{recordId} (deleteDriveParkRecord), DriveParkRecordConflict in api-docs/openapi.yaml DB: DriveParkRecords, DriveParkRecordEquipment, DriveParkRecordPhotos in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • Hard delete: the spec and schema remove the record, its equipment and photo rows, and the photo blobs. A Draft has no signatures or PDF. Confirm no history is needed.
  • Only Draft → 409 record_not_draft for Preuzeto and Vraćeno (docs: they can’t be deleted).
  • A saved return draft can’t be discarded (docs comment, recommended: no). Confirm.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • In one transaction: delete the photo and equipment rows and the record, with PickupCompletedAt IS NULL in the record’s DELETE … WHERE; 0 rows → 404 or 409 (load the record to tell which). Delete the photo blobs after the commit.
  • Success → 204.

3. Verify implementation against spec

  • Status codes and error values match deleteDriveParkRecord.
  • Draft with photos → 204, rows and blobs gone. Preuzeto → 409. Deleting twice → 404.

Done when

A Manager of Vozni park can remove a draft record, and it disappears from the list.

4.19 Backend: Zapisnici photos

Backend task for the Fotografije of the pickup and return forms (POST /drivepark/records/{recordId}/photos, DELETE and download of a photo) – check the API spec and DB structure, validate type, size and count, allow changes only while the stage is open, implement, and verify against the spec.

Docs: Zapisnik o preuzimanju (pickup form), Zapisnik o vraćanju (return form) Spec: POST /drivepark/records/{recordId}/photos (uploadDriveParkRecordPhotos), DELETE /drivepark/records/{recordId}/photos/{photoId} (deleteDriveParkRecordPhoto), GET /drivepark/records/{recordId}/photos/{photoId}/download (downloadDriveParkRecordPhoto), DriveParkRecordPhoto, DriveParkPhotoInvalid, DriveParkRecordConflict in api-docs/openapi.yaml DB: DriveParkRecordPhotos in api/database/pending_schema.sql (-- DRIVE PARK --)

1. Double check API spec and DB structure

  • Rules: JPG, PNG, HEIC; at most 10 MB per file; at most 20 photos per record; removable until the stage is completed. Check the docs, the spec and the schema comment agree. The specification document doesn’t define these rules (docs: recommended values), so confirm them.
  • Count limit: the pickup form says “Up to 20 photos per record”, and the return form says “the same upload rules apply”. The spec follows the docs: 20 per record, both stages together. Confirm it isn’t meant as 20 per stage.
  • Photos need a saved record: the upload needs a record id, so on a new pickup record the frontend must save a draft first when the user picks files. Docs and Figma don’t mention this side effect. Confirm with the project manager.
  • Stage rule: pickup photos only while Draft (record_not_draft), return photos only while Preuzeto (record_not_picked_up).
  • PDF and e-mail: whether photos are part of the PDF is not specified (docs: recommended not). Confirm.
  • HEIC: browsers can’t show HEIC. Decide whether the frontend shows HEIC as a link only, or the backend converts it.
  • Roles and permissions: drivepark.access.

2. Implementation

  • Roles and permissions: require drivepark.access → 403.
  • Upload: validate every file first (content type and extension → 400 file_type_not_allowed; size → 400 file_too_large; existing + new > 20 → 400 too_many_files; no files → 400 validation_error). If any fails, store nothing. Lock the record row (UPDLOCK) while counting, so two parallel uploads can’t pass 20. Upload to the drivepark container (with the validated content type as the blob’s ContentType; the row stores only FileName and BlobName), insert all rows in one statement. Response 201 with the photos.
  • Delete: check the stage rule → 409; delete the row, then the blob. Unknown id or a photo of another record → 404. Success → 204.
  • Download: stream the blob with the content type from the blob’s properties and Content-Disposition with FileName (as downloadBtcTrainingSignatureFile).

3. Verify implementation against spec

  • Routes, stage query parameter, multipart body and response shapes match the three operations.
  • Every error value in DriveParkPhotoInvalid and DriveParkRecordConflict can be produced; no undocumented ones.
  • A PDF → 400 file_type_not_allowed; an 11 MB JPG → 400 file_too_large; a 21st photo → 400 too_many_files, and nothing of that upload is stored.
  • Pickup photo on a Preuzeto record → 409; return photo on a Draft → 409.

Done when

Photos can be added to and removed from an open pickup or return, and opened from the forms and view pages.

5. Vehicle Inventory

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=2065-23407&t=H7jz4hMIphTvr0OG-1

5.1 Vehicle Inventory List

The general vehicle inventory is the master list of all vehicles available across the application (accessible via “Inventar” in the main navigation).
On events vehicles are selected from this list. On Vozni park we show all the vehicles from this list.

The list contains the following information per vehicle:

Brand

Type/Model

Registration plate

Chassis number

Color

Status (Aktivno / Zauzeto / Neispravno)

Status Aktivno / Neispravno is manually chosen from the edit vehicle page. Status Zauzeto is automatically set if the vehicle is used on an event or reserved/picked-up in Vozni park.

User can:

View all vehicles in the inventory (paginated list)

Search vehicles

Filter vehicles by brand

Filter to show only archived or only non-archived vehicles

Add a new vehicle (“Unesi vozilo”)

Edit an existing vehicle (“Uredi vozilo”)

Delete a vehicle from the inventory (“Ukloni vozilo”)

Arhive a vehicle from the inventory (“Arhiviraj vozilo”)

Deleting and archiving a vehicle

If the vehicle has never been added to an event, it can be permanently deleted (“Ukloni vozilo”).

If the vehicle has already been added to an event, it can only be archived (“Arhiviraj vozilo”) – permanent deletion is not available, since its booking history needs to be kept.

The default view on the vehicle inventory list excludes archived vehicles. A filter (“Prikaži arhivirana vozila”) shows only archived vehicles.

Archived vehicles are not shown anywhere else in the app where vehicles can be selected – e.g. when adding vehicles to an event, or when creating a Vozni park reservation.

If the vehicle is “In use” it cannot be deleted or arhiced – this option is not available.

If the vehicle has active reservations for event or vozni park it cannot be deleted or arhived – we show a notification in a modal window.

5.2 Add new vehicle

Clicking “Unesi vozilo” on the general vehicle inventory page opens a form where the following data is entered:

  • Vehicle brand (Marka) - dropdown, mandatory. Register is preset and cannot be edited in the app.
  • Type (Tip) - free text field, mandatory, for example “Golf 8 GTI”
  • Registration plate (Registarska oznaka) - text field, mandatory
  • Chassis number (Broj šasije) - text field, mandatory
  • Color (Boja) - text field, mandatory
  • Usage area (Vrsta korištenja) - radio selection, mandatory
  • Purpose (Namen) - dropdown, mandatory, shown only when “Vozni park” is selected
  • Status (Status) - dropdown, mandatory

Usage area options:

  • Vozni park
  • Događaji

Usage area determines where the vehicle is shown on the calendar in Vozni park. It does not determine which vehicles can be used for which purpose. Vehicles from Vozni park can also be used for events.

Purpose options are predefined and entered by developers. Selected purpose determines how the vehicle is displayed on the Vozni park calendar.

Status options manually selectable by user:

  • Aktivno
  • Neispravno

Status “Zauzeto” is never manually selected. It is applied automatically by the system whenever the vehicle is currently assigned within either Events or Vozni park.

If user selects “Neispravno”, we show notification: “Vozilo neće biti dostupno za rezervacije dok se status ručno ne promijeni nazad u ‘Aktivno’.”

Vehicle cannot be set as “Neispravno” if it has active reservations currently or in the future. Brand manager should remove the vehicle from events / remove reservations, then the vehicle can be set as “Neispravno”.

Saving the form adds the vehicle to the general inventory. Vehicle is also shown on the calendar in Vozni park, and on the selection list of vehicles when an event is edited.

5.3 Edit vehicle

“Uredi vozilo” opens the same form as “Unesi vozilo”, pre-filled with the vehicle’s current data.

Below the form, edit page shows vehicle usage overview: a table of this vehicle’s events and reservations across the whole application (Events and Vozni park).

Past and future events and reservations are shown in reverse order: latest events are shown at the bottom, and future entries are shown at the top.

Vehicle usage overview shows:

  • Type (Vrsta)
  • Name (Naziv događaja / korisnika)
  • Period (Razdoblje)

Type options:

  • Događaj - vehicle is or was added to an event
  • Vozni park - rezervacija - vehicle is or was reserved on Vozni park
  • Vozni park - aktivni najam - vehicle is or was picked-up from Vozni park

Name options:

  • Naziv događaja is shown in case of event
  • Naziv korisnika is shown in case of reservation or pickup from Vozni park

Entries can come from either Vozni park or Events.

5.4 Vehicle detail

Vehicle detail shows the same information as the Edit vehicle page.

User can open Edit vehicle from this page.

6. Assessments

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3054-2447&t=7bsxPorvH8gfocYn-1

Assessments are part of the “Assessment i školovanje” section of the app (side menu: Assessment i školovanja → Assessments).

An assessment is held to choose a new candidate for the position “Prodajni savjetnik”. Candidates are sent to the assessment by a partner (e.g. Auto Zubak).

The goal is a digital flow for the interview phase that saves time on grading and on the final assessment. All board members need access to the application, because some forms are filled out directly in the app.

Roles

There are two roles in Assessments. A user can have only one role at a time. Roles are assigned in user management.

Trainer

Assessment evaluator

Activities

Every assessment has the same fixed set of activities. They are created automatically when the assessment is created and cannot be added or removed manually.

Zajedničke aktivnosti (once per assessment, all candidates in one form):

Aktivnosti po kandidatu (one form per candidate):

Grading scale (Pogodnost)

All graded assessment forms use the same scale, which is shown at the bottom of each form and on the results page:

Data retention

Data is kept for 2 years. Deleting an assessment only hides it in the app; its data (candidates, komisija, grades, notes) stays in the database for the retention period. The same rule applies to BTC školovanje and BTC certificiranje.

6.1 Adding new assessment

The "Kreiraj novi assessment" form (trainer only) with the sections Osnovni podaci (Marka, Partner, Naziv, Datum), Aktivnosti (an optional "Igra uloga – argumentacija" checkbox), Kandidati (1–5) and Komisija (1–10). Saving creates the fixed set of default activities and returns the user to the assessment list with a success toast.

A user with the Trainer role adds a new assessment by clicking “NOV ASSESSMENT” on the assessment list.

Page title: “Kreiraj novi assessment”, with a back arrow that returns to the assessment list.

Osnovni podaci

  • Marka – multiselect dropdown, mandatory
  • Partner – dropdown, mandatory (placeholder “Odaberi”)
  • Naziv – text field, mandatory. Autofilled with the partner selected in the Partner field. The user can then edit it.
  • Datum – date picker, mandatory
  • Info box under the date: “Nakon datuma assessmenta, slučaj će biti označen kao “Prošli”.”
In the Figma edit screen the autofilled Naziv is “Auto Zubak 2026” (partner + year). Confirm whether the year is appended automatically or the partner name alone is used.

Aktivnosti

Section description: “Označite ako će se aktivnost argumentacija provesti. Ako neće, ocjene će se automatski preuzeti iz prvog kontakta.”

In this section the user decides upfront whether the Igranje uloga – argumentacija activity will be carried out during the assessment.

  • Igra uloga – argumentacija – checkbox, optional, unchecked by default
    • Checked – the argumentacija grading form is active and shown on the assessment detail page.
    • Unchecked – the form is not shown on the assessment detail page. On the results page, the argumentacija grades are copied automatically from Igranje uloga – prvi kontakt.

Kandidati

Section description: “Dodajte kandidate koji sudjeluju u assessmentu (max 5)”.

Each candidate row has:

  • Ime i prezime – text field, mandatory (placeholder “npr. Ivan Horvat”)
  • E-mail – text field, optional (placeholder npr. ivan.horvat@primjer.hr)

Rules:

  • At least one candidate must be added. A maximum of 5 candidates can be added.
  • The “+ Dodaj polje” button adds another candidate row.
  • Every additional row has a “–” button that removes it. The first row cannot be removed.
  • Candidates are stored per assessment only. They are not linked to a shared candidate database.

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3265-18341

Komisija

Section description: “Dodajte članove komisije koji provode assessment, uključujući sebe ako ste dio komisije”.

Each row has:

  • Odaberi komisiju – dropdown, mandatory (placeholder “Odaberi”). Each option is shown as “Ime i prezime - E-mail - Role”, e.g. “Maša Perić - masaperic@gmail.com - Trainer”. The role is either Trainer or Assessment evaluator.

Rules:

  • At least one board member must be added. A maximum of 10 board members can be added.
  • The “+ Dodaj polje” button adds another member row.
  • Every additional row has a “–” button that removes it.
  • Board members must first be added as users in user management, with the Trainer or Assessment evaluator role. Only then can they be selected here.
Not specified: should a user who is already selected in another row be excluded from the dropdown? Recommended: yes, to prevent duplicates.

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3899-6721

Default activities

When a new assessment is created, the following activities are added automatically. They are the same for every assessment and cannot be added or removed manually.

Zajedničke aktivnosti (added once per assessment):

  • Grupna diskusija
  • Igranje uloga – prvi kontakt
  • Igranje uloga – argumentacija (only if the checkbox under Aktivnosti was checked)

Individual activities (added for each candidate):

  • Predstavljanje kandidata
  • Strukturirani intervju – bilješke
  • Strukturirani intervju – ocijene

Saving

  • The “KREIRAJ ASSESSMENT” button in the sticky footer saves the assessment.
  • Mandatory fields are validated before saving.
  • When the assessment is created, the user is taken to the assessment list and a success toast notification is shown.

6.2 Assessment list

List of assessments across all brands with search, date range, brand and status filters, a "Nov assessment" button, and a per-row actions menu (Detalji / Uredi / Ukloni assessment) with a delete confirmation modal. Status (Aktivni / Nadolazeći / Prošli) is calculated from the assessment date.

The assessment list displays assessments across all brands. It is opened from the side menu: Assessment i školovanja → Assessments.

Page title: “Assessments”. Breadcrumb: Assessments.

List columns

  • Naziv assessmenta – name of the assessment
  • Marka – brand(s). An assessment for several brands shows them comma separated (e.g. “Volkswagen, Škoda”)
  • Datum – date of the assessment (dd.mm.yyyy)
  • Status – tag with one of: Aktivni, Nadolazeći, Prošli
  • Actions menu (three dots icon “…”)

Status

Status is set automatically from the assessment date:

  • Aktivni – the assessment is scheduled for today
  • Nadolazeći – the assessment is in the future
  • Prošli – the assessment is in the past

Sorting

The list is sorted by status first (Aktivni → Nadolazeći → Prošli). Within each status group it is sorted by date, ascending.

Search and filters

  • Search field “Pretraži po nazivu assessmenta ili kandidatu” – searches by assessment name and by candidate name
  • Date range picker (dd.mm.yyyy - dd.mm.yyyy) – filters by assessment date
  • MARKA – filter by brand
  • PRIKAŽI – filter by status, with the options:
    • All
    • Aktivni
    • Nadolazeći
    • Prošli

By default all statuses are shown.

Pagination

The design shows pagination under the list (“Prikazuje se 1-14 od 120” and page numbers).

The spec does not mention pagination for the assessment list, but Figma shows it. Confirm the page size.

Permissions

Action on Assessment listTrainerAssessment evaluator
View assessmentsYes (all assessments)Yes (only the ones they are added to)
Search assessments by nameYesYes
Search assessments by candidate nameYesYes
Filter by date rangeYesYes
Filter by brand (MARKA)YesYes
Filter by status (PRIKAŽI)YesYes
Add a new assessment (“NOV ASSESSMENT” button)YesNo
View assessment detailYesYes
Edit assessment (Aktivni / Nadolazeći only)YesNo
Delete assessment (Nadolazeći only)YesNo

Actions menu

Clicking the three dots icon on a row opens a menu. Its options depend on the status (and the role – an assessment evaluator only sees “Detalji assessmenta”):

StatusOptions
AktivniDetalji assessmenta, Uredi assessment
NadolazećiDetalji assessmenta, Uredi assessment, Ukloni assessment (red, with a trash icon)
ProšliDetalji assessmenta

Rules:

  • Only assessments with status Aktivni or Nadolazeći can be edited. Prošli assessments cannot be edited.
  • Only assessments with status Nadolazeći can be deleted. Aktivni and Prošli assessments cannot be deleted.

Delete confirmation modal

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3265-18685

  • Warning icon, title “Ukloni assessment?”, with the assessment name as a subtitle (e.g. “Assessment 2026/3”)
  • Text: “Jeste li sigurni da želite ukloniti ovaj assessment?” (Figma has the typo “ovi”)
  • Buttons:
    • Odustani – closes the modal without deleting
    • Ukloni (red) – deletes the assessment. It disappears from the app, but its data is kept (see Data retention).
  • Close icon (x) in the top right corner – closes the modal without deleting

6.3 Assessment detail

The assessment detail page, with a header (name, status, brand, date, komisija), links to Rezultati assessmenta and (trainer only) to both Bilješke pages, a Zajedničke aktivnosti table, and a collapsible "Aktivnosti po kandidatu" list. Each activity shows the current user's Zaključeno / Nije zaključeno status and an "Otvori" button. What is visible depends on the role (Trainer / Assessment evaluator) and on whether argumentacija is enabled.

The assessment detail page is visible to all trainers, and to the assessment evaluators who are added to this assessment.

Breadcrumb: Assessments > {assessment name}. A “Natrag” link returns to the assessment list.

Permissions

Action on Assessment detail pageTrainerAssessment evaluator
Access the assessment detail pageYesYes (must be added to the assessment)
View header (name, status, brand, date, komisija list)YesYes
Edit the assessment (“UREDI ASSESSMENT” button)YesNo
Go to the results page (“Rezultati assessmenta”)YesYes
View Grupna diskusija – Bilješke (aggregated notes)YesNo
View Predstavljanje kandidata – Bilješke (aggregated notes)YesNo
Open / fill the Grupna diskusija formYesYes
Open / fill the Igranje uloga – prvi kontakt formYesYes
Open / fill the Igranje uloga – argumentacija form (if enabled)YesYes
Open / fill the Predstavljanje kandidata formYesYes
Open / fill the Strukturirani intervju – bilješke formYesNo
Open / fill the Strukturirani intervju – ocijene formYesYes
Edit own forms after the form status is ZaključenoYesYes
Not specified: can board members open and fill forms while the assessment is still Nadolazeći, or after it is Prošli? The spec says forms can still be edited after they are Zaključeno, which suggests editing is always allowed.
Not specified: can a trainer who is not in the komisija of this assessment fill in forms? Every form stores grades per board member, so the recommendation is that only komisija members see the “Otvori” button and trainers outside the komisija get read-only access to the page, the results and the Bilješke pages.

Page sections

1. Header

  • Assessment name with a status tag (Aktivni / Nadolazeći / Prošli)
  • Brand(s) · date
  • “Komisija:” followed by a comma-separated list of the board members’ names
  • “UREDI ASSESSMENT” button (Trainer only) – opens Uredi assessment

2. Rezultati assessmenta

A full-width link card, “Rezultati assessmenta →”, that opens the results page. Visible to both roles.

3. Grupna diskusija – Bilješke (Trainer only)

A link card that opens Grupna diskusija – Bilješke.

4. Predstavljanje kandidata – Bilješke (Trainer only)

A link card that opens Predstavljanje kandidata – Bilješke.

5. Zajedničke aktivnosti

Activities that are carried out for all candidates together, or that the board member grades for all candidates in the same form.

The table columns are Naziv aktivnosti, Status and Akcija. The rows are:

  • Grupna diskusija
  • Igra uloga – prvi kontakt
  • Igra uloga – argumentacija (only shown if it was checked when the assessment was set up)

6. Aktivnosti po kandidatu

This section lists all candidates added to the assessment. Each candidate is a collapsible row that shows:

  • An avatar with the candidate’s initials
  • The candidate’s name
  • The candidate’s email · partner

When a row is expanded, it shows the candidate’s individual activities:

  • Predstavljanje kandidata
  • Strukturirani intervju – bilješke (Trainer only)
  • Strukturirani intervju – ocijene
Not specified: are the candidate rows collapsed or expanded by default? Figma shows the first candidate expanded and the others collapsed.
The labels for Strukturirani intervju – bilješke differ between Figma screens: “(predsjednik komisije)” and “(samo trener)”. Pick one label.

Activity status and the Otvori button

Each activity row (in both sections) shows:

  • Status for the current user:
    • Zaključeno – green, with a check icon
    • Nije zaključeno – grey, with a clock icon
  • An “Otvori” button that opens the form

When a form counts as Zaključeno is defined on each form’s page under Assessment forms. The status is only an indicator, used mainly on the results page to show whether every board member has finished grading. Board members can still edit their forms after a form is Zaključeno.

Each board member fills in their own grades independently and sees only their own input in each form.

Figma screens

The “argumentacija not enabled” screen has no Bilješke links and no “UREDI ASSESSMENT” button, but it does show Strukturirani intervju – bilješke, so it mixes the two role views. Its only purpose is to show the hidden argumentacija row. The permissions table above is what applies.

6.4 Editing existing assessment

The "Uredi assessment" form (trainer only, Aktivni and Nadolazeći assessments only). Same fields as Adding new assessment, prefilled, with the "SPREMI ASSESSMENT" CTA. Candidates who already have grades cannot be removed. Removed board members keep their grades. Disabling argumentacija mid-progress deletes its data. Saving returns the user to the assessment detail page with a success toast.

A user with the Trainer role edits an assessment either from the actions menu on the assessment list (“Uredi assessment”) or with the “UREDI ASSESSMENT” button on the assessment detail page.

Only assessments with status Aktivni or Nadolazeći can be edited.

The form has the same fields and validation as Adding new assessment, prefilled with the current data, with these differences:

  • Page title is “Uredi assessment”
  • The CTA button is “SPREMI ASSESSMENT”
The spec says the page title is “Editiraj assessment”, but Figma and the list actions menu use “Uredi assessment”. These docs follow Figma.

Rules

  • A candidate who already has any grades cannot be removed. The “–” button is not available for that candidate.
  • A board member can be removed while the assessment is in progress. Grades and notes already entered by that member stay in the app.
  • If the trainer unchecks “Igra uloga – argumentacija” while the assessment is in progress, the data already entered in the argumentacija form is lost. From then on, the results page uses the prvi kontakt grades for argumentacija.
  • Adding a candidate creates their individual activities (Predstavljanje kandidata, Strukturirani intervju – bilješke, Strukturirani intervju – ocijene).
Not specified: should notes (Predstavljanje kandidata, Grupna diskusija bilješke, Strukturirani intervju – bilješke) also block removing a candidate, or only grades?
Recommended: show a confirmation before saving when argumentacija is unchecked and grades already exist for it, because the data will be lost.

Saving

When the assessment is saved, the user is taken to the assessment detail page and a success toast notification is shown.

6.5 Assessment forms

Rules shared by all assessment forms. Saving works through "Spremi" and "Spremi i zatvori" with a toast. Every board member's input is private to them. The grading table has white input rows for sub-criteria and grey category-average rows, a POGODNOST row and the 0–3 scale with one decimal. Shared and individual forms have different layouts.

These rules apply to every assessment form. Each form’s own page describes its criteria, its status rule and anything specific to it.

Common layout

  • Breadcrumb: Assessments > {assessment name}
  • “Natrag” link that returns to the assessment detail page
  • Title: the form name. Subtitle: the assessment name.
  • Sticky footer with:
    • the current candidate (avatar with initials + name), for individual forms only
    • “SPREMI I ZATVORI” – saves and returns to the assessment detail page
    • “SPREMI” – saves and stays on the form

A success toast notification is shown every time a form is saved.

Data visibility

Each board member fills in their own grades and notes independently and sees only their own input in each form. Averages across all board members are shown only on the results page. Notes from all board members can be seen only on the Bilješke pages, which are trainer only.

Shared and individual forms

  • Shared forms (Zajedničke aktivnosti) – Grupna diskusija, Igranje uloga – prvi kontakt and Igranje uloga – argumentacija. All candidates are graded in one form, as a table with candidates as columns and criteria as rows.
  • Individual forms (Aktivnosti po kandidatu) – Predstavljanje kandidata, Strukturirani intervju – bilješke and Strukturirani intervju – ocijene. Each form is filled in for one candidate, who is shown in the sticky footer. To move to the next candidate, the board member goes back to the assessment detail page and opens the form for that candidate.

Grading table

Used by Grupna diskusija, Igranje uloga – prvi kontakt, Igranje uloga – argumentacija and Strukturirani intervju – ocijene.

The first column header is “Kriterij / Kandidat”. The other columns are the candidates: all candidates for shared forms, one candidate for Strukturirani intervju – ocijene.

There are two types of rows:

  • White rows (sub-criteria) – input fields. The board member does not need to fill in every sub-criterion. At least one sub-criterion per category must be filled in for the form to be Zaključeno. An empty input shows “—”.
  • Grey rows (categories, e.g. “Pojava / ponašanje”) – read-only. The value is calculated automatically as the average of the sub-criteria filled in within that category. If only one sub-criterion is filled in, the grey row shows that value. If none are filled in, it shows “—”.

The POGODNOST row at the bottom is read-only. It shows the overall average across all categories (the grey rows) for each candidate. This value is used as the final grade for the activity in the results page, averaged across all board members.

In Figma, POGODNOST already shows a value when only one category has values (e.g. Pojava / ponašanje = 2 → POGODNOST = 2). This means POGODNOST is the average of the categories that have a value, with empty categories ignored. Please confirm.

Input validation

  • Grades are on a scale of 0–3, and one decimal is allowed (e.g. 2,5).
  • No other numbers or characters can be entered.
  • Values are shown with a decimal comma.
Recommended: accept both “,” and “.” as the decimal separator while typing, and show “,” as the separator.

Grading scale

Shown at the bottom of each grading form: “Pogodnost: 0 Nepogodan, 1 Ne, sa rezervom, 2 Da, sa rezervom, 3 Da”.

6.6 Form – Grupna diskusija

A shared grading form in which all candidates are graded in one table (candidates × criteria) across 5 categories with 12 sub-criteria, on a 0–3 scale with one decimal. Category averages and POGODNOST are calculated automatically. Per-candidate Bilješke notes feed the Grupna diskusija – Bilješke page. The form is Zaključeno when every category has at least one grade for every candidate.

Grupna diskusija is a shared activity: all candidates are evaluated at the same time, in the same form. It is opened with “Otvori” under Zajedničke aktivnosti on the assessment detail page. It is available to both Trainer and Assessment evaluator.

The general rules (layout, saving, visibility, grading table, validation) are described in Assessment forms.

Title: “Grupna diskusija”. Subtitle: the assessment name.

Grading table

Candidates are columns. Grey rows are categories (read-only averages) and white rows are sub-criteria (inputs, 0–3, one decimal).

Pojava / ponašanje

  • Kandidat je propisno odjeven i prijatne je pojave.
  • Kandidat ostavlja prijatan i simpatičan utisak.
  • Kandidat iskazuje sigurnost i samopouzdanje ali bez arogancije.

Društvenost / Empatija

  • Kandidat pokazuje interes za poglede i mišljenja partnera u diskusiji.
  • Kandidat uključuje ostale učesnike u diskusiju.

Govorne sposobnosti

  • Kandidat koristi odgovarajući način izražavanja.
  • Kandidat dozvoljava ostalim učesnicima da dovrše svoje misli i nije dominantan.
  • Kandidat održava kontakt očima s drugim učesnicima.

Sposobnost uvjeravanja / Upornost

  • Kandidat zauzima jasan stav pomoću jasnih izjava.
  • Kandidat daje razložne i razumljive argumente za njegov stav i viđenje.

Posvećenost / samo-motiviranost

  • Kandidat pokazuje angažiranost i entuzijazam u diskusiji.
  • Kandidat iskazuje pozitivan stav u odnosu na temu diskusije.

POGODNOST – the average of the category rows for each candidate. It is used as the Grupna diskusija grade on the results page.

Bilješke

Below the grading table, the Bilješke section has one free-text field per candidate (column header: the candidate’s name, placeholder “Bilješke…”). The field is optional and does not affect the form status.

These notes are shown to the trainer on the Grupna diskusija – Bilješke page.

The grading scale is shown at the bottom of the form.

Status of the form

The form is Zaključeno for the current board member when, for every candidate, every category has at least one sub-criterion filled in (so every grey row has a value). Otherwise it is Nije zaključeno.

“SPREMI I ZATVORI” and “SPREMI”. See Assessment forms.

6.7 Form – Igranje uloga – prvi kontakt

A shared grading form for the first-contact role play. The role play is done one candidate at a time, but all candidates are graded in one table (candidates × criteria) across 7 categories with 16 sub-criteria, on a 0–3 scale with one decimal. Category averages and POGODNOST are calculated automatically. The form is Zaključeno when every category has at least one grade for every candidate. Its grades are also used for argumentacija when argumentacija is not enabled.

Igranje uloga – prvi kontakt is carried out one candidate at a time, but all candidates are graded in the same form. It is opened with “Otvori” under Zajedničke aktivnosti on the assessment detail page. It is available to both Trainer and Assessment evaluator.

The general rules (layout, saving, visibility, grading table, validation) are described in Assessment forms.

Title: “Igra uloga - prvi kontakt”. Subtitle: the assessment name.

Grading table

Candidates are columns. Grey rows are categories (read-only averages) and white rows are sub-criteria (inputs, 0–3, one decimal).

Zadatak

  • Sljedeći sastanak je dogovoren.

Orijentacija prema kupcu

  • Kandidat saznaje informacije o zahtjevima kupca.
  • Kandidat se koncentrira zahtjeve kupca (stavlja u fokus zahtjeve kupca).

Društvenost / Empatija

  • Kandidat je otvoren i bez predrasuda.
  • Kandidat postiže otvoreni razgovor s kupcem veoma brzo.
  • Kandidat pokazuje interes za situaciju u kojoj se nalazi kupac.

Posvećenost / samo-motiviranost

  • Kandidat izvršava zadatak samouvjereno i sa entuzijazmom.
  • Kandidat je sve vrijeme koncentriran na ostvarenje svog cilja.

Pojava / ponašanje

  • Kandidat ostavlja prijatan i simpatičan utisak.
  • Kandidat pokazuje sigurnost i samopouzdanje.

Tehnika i sposobnost

  • Kandidat vodi razgovor na dobro strukturiran način.
  • Kandidat bira i koristi odgovarajuće podatke o vozilu.
  • Kandidat dobro poznaje Škoda proizvode.

Pripreme i razgovor za ocjenjivanje

  • Kandidat je u stanju da odgovara na primjedbe.
  • Kandidat se drži zadane teme i ne luta.
  • Kandidat prihvaća kritike.

POGODNOST – the average of the category rows for each candidate. It is used as the Igra uloga – prvi kontakt grade on the results page.

“Kandidat dobro poznaje Škoda proizvode.” names a specific brand. Should the brand name be filled in from the assessment’s brand(s), or stay fixed text? An assessment can have several brands.

The grading scale is shown at the bottom of the form.

Status of the form

The form is Zaključeno for the current board member when, for every candidate, every category has at least one sub-criterion filled in. Otherwise it is Nije zaključeno.

Relation to argumentacija

If Igra uloga – argumentacija is not enabled for the assessment, the prvi kontakt grades are also used as the argumentacija grades on the results page.

“SPREMI I ZATVORI” and “SPREMI”. See Assessment forms.

6.8 Form – Igranje uloga – argumentacija

An optional shared grading form, shown only if "Igra uloga – argumentacija" was checked when the assessment was set up. All candidates are graded in one table across 7 categories with 16 sub-criteria, on a 0–3 scale with one decimal. When argumentacija is disabled, the results page copies the prvi kontakt grades and the form is always Nije zaključeno.

Igranje uloga – argumentacija is a shared activity: all candidates are graded in the same form. It is opened with “Otvori” under Zajedničke aktivnosti on the assessment detail page. It is available to both Trainer and Assessment evaluator.

The general rules (layout, saving, visibility, grading table, validation) are described in Assessment forms.

Title: “Igra uloga - argumentacija”. Subtitle: the assessment name.

Optional form

This is the only form the trainer can turn on or off, with the “Igra uloga – argumentacija” checkbox under Aktivnosti when the assessment is created or edited.

  • Checked – the form is shown under Zajedničke aktivnosti on the assessment detail page and is filled in like the other grading forms.
  • Not checked – the form is not shown on the assessment detail page. On the results page, the Igra uloga – argumentacija row is still shown, with the values copied from Igra uloga – prvi kontakt.
  • If the trainer unchecks argumentacija while the assessment is in progress, the data already entered in this form is lost.

Grading table

Candidates are columns. Grey rows are categories (read-only averages) and white rows are sub-criteria (inputs, 0–3, one decimal).

Zadatak

  • Test vožnja ili novi sastanak su dogovoreni.

Orijentacija prema kupcu

  • Kandidat koristi informacije prikupljene kroz prvi kontakt.
  • Kandidat prikuplja informacije o daljnjim zahtjevima-potrebama kupca.
  • Kandidat stavlja zahtjeve - potrebe kupca u centar pažnje.
  • Kandidat koristi informaciju na način da ih koristi za rješavanje primjedbi.

Društvenost / Empatija

  • Kandidat kroz razgovor održava pozitivan i blizak odnos s kupcem.
  • Kandidat pokazuje interes i razumijevanje za situaciju kupca.

Vještina govora i izražavanja

  • Kandidat vodi komunikaciju kao dijalog (ne monolog).
  • Kandidat govori jezikom koji je razumljiv za kupca (prilagođava se kupcu).

Posvećenost / samo-motiviranost

  • Kandidat pokazuje motiviranost za temu “Automobili”.

Tehnika i sposobnost

  • Kandidat zna kako prezentirati vozilo na koristan način.
  • Kandidat posjeduje osnovno znanje iz tehnike automobila.
  • Kandidat dobro poznaje Škoda proizvode.

Pripreme i razgovor za ocjenjivanje

  • Kandidat je u stanju jasno formulirati ciljeve-zadatke.
  • Kandidat se drži zadane teme i ne luta.
  • Kandidat prihvaća kritike.

POGODNOST – the average of the category rows for each candidate. It is used as the Igra uloga – argumentacija grade on the results page.

“Kandidat dobro poznaje Škoda proizvode.” names a specific brand. Should the brand name be filled in from the assessment’s brand(s), or stay fixed text?

The grading scale is shown at the bottom of the form.

Status of the form

  • Enabled – the form is Zaključeno for the current board member when, for every candidate, every category has at least one sub-criterion filled in. Otherwise it is Nije zaključeno.
  • Not enabled – the form is always Nije zaključeno.

“SPREMI I ZATVORI” and “SPREMI”. See Assessment forms.

6.9 Form – Predstavljanje kandidata

An individual notes form, filled in per candidate, with 5 open questions as free-text fields. Some questions have a hint for the board member. There are no grades. The form is Zaključeno when all 5 fields are filled in. Notes feed the Predstavljanje kandidata – Bilješke page.

Predstavljanje kandidata is an individual activity: the form is filled in separately for each candidate. It is opened with “Otvori” next to the candidate under Aktivnosti po kandidatu on the assessment detail page. It is available to both Trainer and Assessment evaluator.

The current candidate (avatar + name) is shown in the sticky footer. To move to the next candidate, the board member goes back to the assessment detail page and opens the form for that candidate.

The general rules (layout, saving, visibility) are described in Assessment forms.

Title: “Predstavljanje kandidata”. Subtitle: the assessment name.

Questions

The form has 5 open questions. The board member uses them to take notes during the candidate’s self-presentation. Each question has a multi-line text field (placeholder “Bilješke…”).

  1. Predstavite se:
  2. Gdje ste i što ste do sada radili?
  3. Što Vas je motiviralo da se prijavite na ovaj Oglas?
    • Hint box (blue) under the field: “Uputa za člana komisije: “Kvalificirani odgovor: zato što volim automobile i zato što me zanima rad s automobilima.””
  4. Kako Vi vidite tržište automobila u Hrvatskoj i posebno (navesti određeno tržišno područje)?
  5. Možete li nam reći nekoliko rečenica o tome koji su to glavni dijelovi koji čine automobil?

Some questions have a hint for the board member (“Uputa za člana komisije”) with an example of a qualifying answer. In Figma only question 3 has a hint.

Grading

This form has no grades. All fields are free-text notes.

Status of the form

The form is Zaključeno for the current board member and candidate when all 5 fields are filled in. Otherwise it is Nije zaključeno.

Additional

The notes are shown to the trainer on the Predstavljanje kandidata – Bilješke page.

The current candidate, then “SPREMI I ZATVORI” and “SPREMI”. See Assessment forms.

6.10 Form – Strukturirani intervju – bilješke

An individual, trainer-only notes form for the structured interview, filled in per candidate. It spans 6 pages (navigated with NATRAG / NAPRIJED) with static guidance text and 21 free-text note fields. There are no grades. Notes are personal and are not shown anywhere else. The form is Zaključeno when every field on all 6 pages is filled in.

Strukturirani intervju – bilješke is an individual activity. Only the Trainer role sees this form. It is opened with “Otvori” next to the candidate under Aktivnosti po kandidatu on the assessment detail page.

The form is filled in separately for each candidate. The current candidate (avatar + name) is shown in the sticky footer. To move to the next candidate, the trainer goes back to the assessment detail page and opens the form for that candidate.

The general rules (layout, saving, visibility) are described in Assessment forms. This form uses the back link “Natrag na assessment”.

Pagination

  • The form is split across 6 pages. The title shows the current page: “Strukturirani intervju 1/6” … “Strukturirani intervju 6/6”. Subtitle: the assessment name.
  • “NATRAG” and “NAPRIJED” buttons under the content move between pages.
  • On page 1/6, “NATRAG” is disabled.
  • On page 6/6, “NAPRIJED” is disabled. The trainer saves and leaves the form with “SPREMI I ZATVORI”.
Not specified: is the data on the current page saved automatically when the trainer moves to another page with NATRAG / NAPRIJED? Recommended: keep entered values while moving between pages (one form state) and save on “SPREMI” / “SPREMI I ZATVORI”. Optionally, warn about unsaved changes before leaving the form.

All note fields are multi-line text fields with the placeholder “Bilješke…”. “Moguće dodatno pitanje” lines are static hints for the interviewer.

Page 1/6 – UVOD

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-1303

Static text:

1. Informacija za prisutne članove komisije o proceduri i ciljevima intervjua

Članovi komisije moraju biti pripremljeni za intervju:

  • Objašnjenje ciljeva intervjua → „Mi ćemo govoriti o stavovima i ponašanju koji su neophodni u prodaji"
  • Objašnjenje zadataka članova komisije (samo slušanje, evidentiranje zapisa)
  • Objašnjenje tabele za ocjenjivanje
  • Ocjena intervjua se daje tek nakon završetka intervjua; npr. nema ocjenjivanja za vrijeme intervjua
  • Otvorene teme iz salona mogu se razjasniti s kandidatom nakon konačnog razgovora.

2. Informacije za kandidate o pravilima i načinu izvođenja intervjua

  • Za vrijeme predstojećeg intervjua svim kandidatima ćemo postavljati ista pitanja.
  • Odgovorite samo na pitanja koja ste razumjeli. Ako se dvoumite, molim da me pitate.
  • Ne morate odgovoriti na sva pitanja, ukoliko se ne osjećate ugodno.
  • Članovi komisije i ja ćemo samo zapisivati informacije i skupljati Vaše odgovore, u toku intervjua nema ocjenjivanja Vaših odgovora.
  • Nakon ocjene intervjua, podaci će biti uništeni.
  • Prisutni članovi komisije neće se uključivati u naš intervju. Oni će samo slušati i zapisivati Vaše odgovore. Nakon završetka intervjua, radi bolje objektivnosti i oni će učestvovati u ocjeni intervjua.
  • Intervju će trajati između 60 i 90 minuta.
  • Nakon ocjenjivanja, rezultat intervjua će biti komentiran kroz razgovor s Vama.

3. Informacija za kandidata o ciljevima intervjua

  • U intervju-u želimo Vam dati priliku da pokažete osobine i stavove koji su važni za prodaju.

4. Posljednje pitanje nakon uputa danih kandidatu

  • Ima li još nešto što Vam nije jasno?
  • Sada, nakon što sam Vam dao ove informacije, ima li daljnjih pitanja ili zahtjeva koje biste željeli postaviti?

5. Prijelaz na intervju

  • Vidim da razumijete proceduru.
  • Sada ću Vam postaviti prvo pitanje kako bih Vam pomogao da počnete.

6. Prvo pitanje

  • Prije svega, kažite mi nešto o Vašoj prethodnoj karijeri.
  • Postavljati po potrebi otvorena pitanja da bi kandidat objasnio izjave detaljnije.
  • NE POSTAVLJATI PREVIŠE PITANJA!

Note fields: 1 (after “6. Prvo pitanje”).

Page 2/6 – GLAVNA FAZA INTERVJUA

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-1634

7. Predanost/Odnos prema preuzetim obavezama – 5 note fields, one per question:

  • a) Voditelj prodaje veoma često koristi sljedeću rečenicu: “Prodajni savjetnik je uvijek na dužnosti.” “Kako Vi doživljavate izjavu i što Vam ta rečenica znači u kontekstu Vašeg profesionalnog pristupa poslu?”
    • Moguće dodatno pitanje: → Kako shvaćate ovu frazu? → Što bi moglo prouzrokovati nelagodu za Vas?
  • b) Danas ste na sastanku s Vašim voditeljem prodaje. Unatoč teškoj situaciji na tržištu, on od Vas traži da povećate Vašu prodaju za 20%. Kako ćete reagirati?
    • Moguće dodatno pitanje: → Što ćete učiniti ako date sve o sebe i svejedno ne ostvarite traženi rezultat?
  • c) Kao najuspješniji prodajni savjetnik u salonu, dobivate ponudu za poziciju voditelja prodaje. Što Vam prvo prolazi kroz glavu i kako procjenjujete jeste li spremni za taj korak?
    • Moguće dodatno pitanje: → Koje će posljedice za Vas donijeti nova pozicija?
  • d) Na koji svoj profesionalni i privatni uspjeh ste najponosniji i zašto?
    • Moguće dodatno pitanje: → Kako ste ostvarili taj uspjeh?
  • e) Možete li opisati situaciju u kojoj ste morali uložiti izniman osobni trud kako biste postigli cilj ili riješili izazov? Što Vas je motiviralo da ustrajete?
    • Moguće dodatno pitanje: → Kako ste se osjećali u toj situaciji? → Na koji način i pomoću kojih sredstava/alata ste riješili situaciju?

Page 3/6

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-1843

8. Društvenost/Izražena osjećajnost – 4 note fields:

  • a) Zamislite da ste upravo započeli s radom u salonu automobila. Na koji način biste se potrudili uspostaviti dobar odnos s novim kolegama i postati dio tima?
    • Moguće dodatno pitanje: → Koji način Vam se čini najprirodniji ili najlakši? → Što biste učinili ako kolega ne pokaže interes za razgovor ili Vas u početku odbije?
  • b) Zamislite da dolazite na prodajni trening i u predvorju konferencijske dvorane susrećete sudionike koje još ne poznajete. Kako se u tom trenutku osjećate i bi ste li pristupili drugim sudionicima?
    • Moguće dodatno pitanje: → Jeste li i kako ste ostvarili kontakt s drugim učesnicima na ovom događaju?
  • c) Koje sve prilike i situacije prepoznajete kao dobru šansu za uspostavljanje kontakta s nepoznatim osobama – bilo u privatnom ili poslovnom okruženju? Slobodno navedite neke primjere.
    • Moguće dodatno pitanje: → Kako biste stupili u kontakt s nepoznatim kupcima?
  • d) Šećete po shopping centru i primijetite da se izlog upravo uređuje od strane ljudi iz reklamne agencije. Koja su Vaša prva razmišljanja u toj situaciji? Koje emocije to pobuđuje?
    • Moguće dodatno pitanje: → Što za Vas znači reklama i dobar izlog?

Page 4/6

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-2079

9. Stabilnost u teškim situacijama – 5 note fields:

  • a) Zamislite sljedeće. U prethodnom tjednu nekoliko kupaca je neočekivano odustalo od kupovine vozila, a za koje ste bili uvjereni kako će kupiti vozilo kod Vas. Kako biste se osjećali u takvoj situaciji?
    • Moguće dodatno pitanje: → Kako biste pristupili rješavanju te situacije? → Je li bilo sličnih situacija u Vašem životu i kako ste na njih odgovorili?
  • b) A sada obrnuta situacija, prodali ste vozilo kupcu za koji vaš kolega tvrdi da je njegov prijatelj, i da je ta prodaja trebala pripasti njemu. Kako bi ste se postavili u navedenoj situaciji i koje bi korake konkretno napravili?
  • c) Svakodnevni život i posao podrazumijevaju da činimo i propuste. Molim da opišete jednu takvu situaciju u Vašem životu.
    • Moguće dodatno pitanje: → Kako ste se osjećali u takvoj situaciji? → Kako ste riješili tu situaciju? → Koje alate koristite za rješavanje sličnih situacija?
  • d) Koje izazove ili teške situacije očekujete da biste mogli susresti radeći u ovom poslu (ili u ovoj tvrtki) i kako biste se pripremili za njih?
    • Moguće dodatno pitanje: → Zašto smatrate da su te situacije teške? → Što ćete učiniti da izađete iz takve situacije?
  • e) Ispričajte nam o situaciji u kojoj ste se našli pod pritiskom na samom početku nekog izazova ili zadatka. Koje ste korake poduzeli da biste se vratili u stabilnu i kontroliranu poziciju?
    • Moguće dodatno pitanje: → Kako se osjećate u takvim situacijama? → Što se dešava tada?

Page 5/6

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-2297

10. Orijentacija prema cilju – 5 note fields:

  • a) Koje ciljeve želite ostvariti u ovom novom životnom razdoblju i u novoj karijeri koju započinjete?
    • Moguće dodatno pitanje: → Konkretno kod ovog trgovca Škoda automobilima?
  • b) Koji su Vaši glavni razlozi vezani za promjenu tvrtke ili radnog mjesta? Što želite postići tim potezom?
    • Moguće dodatno pitanje: → Koja očekivanja imate od ovog radnog mjesta?
  • c) Koje ste si ciljeve postavljali u karijeri i privatnom životu do sada, i kako ste ih ostvarili?
    • Moguće dodatno pitanje: → Koji od navedenih ciljeva ste ostvarili? → Na koji način ste to napravili? → Koji od navedenih ciljeva niste ostvarili? → Što Vas je spriječilo da to učinite?
  • d) Zamislite da ste već tri godine zaposlenik u ovoj tvrtki. Kako biste opisali svoju ulogu, položaj i profesionalni razvoj u tom razdoblju?
    • Moguće dodatno pitanje: → Koja je Vaša profesionalna pozicija u tom budućem vremenu? → Što je s pozicijom u privatnom životu?
  • e) Molim Vas da poredate sljedeće stavke prema njihovoj važnosti za Vas, od najvažnije do najmanje važne: RADNA ATMOSFERA, PLAĆA, MOGUĆNOST NAPREDOVANJA
Question 10 a) names a specific brand (“Škoda automobilima”). Should it be filled in from the assessment’s brand(s)?

Page 6/6

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3126-2457

Static text:

11. Završetak intervjua

  • Hvala vam puno na otvorenom i ugodnom intervjuu.
  • Sada, nam treba malo vremena da ocijenimo Vaš intervju. Molimo da budete strpljivi.
  • Opet ćemo Vas pozvati za završni razgovor.

12. Ocjena intervjua – 1 note field.

Grading

This form has no grades. All fields are free-text notes. The interview is graded in the separate Strukturirani intervju – ocijene form.

Status of the form

The form is Zaključeno when every free-text field on all 6 pages is filled in (21 fields: 1 + 5 + 4 + 5 + 5 + 1). Otherwise it is Nije zaključeno.

Additional

Notes are personal: each trainer sees only their own notes. They are not shown anywhere else in the application.

The current candidate, then “SPREMI I ZATVORI” and “SPREMI” (available on every page). See Assessment forms.

6.11 Form – Strukturirani intervju – ocijene

An individual grading form for the structured interview, filled in per candidate. It is a single grading table with one candidate column, 6 categories and 36 sub-criteria, on a 0–3 scale with one decimal. Category averages and POGODNOST are calculated automatically, and POGODNOST is used as the Intervju grade on the results page. The form is Zaključeno when every category has at least one grade.

Strukturirani intervju – ocijene is an individual grading form, filled in separately for each candidate. It is opened with “Otvori” next to the candidate under Aktivnosti po kandidatu on the assessment detail page. It is available to both Trainer and Assessment evaluator.

The current candidate (avatar + name) is shown in the sticky footer. To move to the next candidate, the board member goes back to the assessment detail page and opens the form for that candidate.

The general rules (layout, saving, visibility, grading table, validation) are described in Assessment forms.

Title: “Strukturirani intervju - ocijene”. Subtitle: the assessment name.

Grading table

The table has one column, for the current candidate. Grey rows are categories (read-only averages) and white rows are sub-criteria (inputs, 0–3, one decimal).

Predanost / Odnos prema preuzetim obavezama

  • Doživljava zadatke kao izazov i priliku za sebe
  • Izvršava zadatke pokazujući veliku posvećenost / istrajnost
  • Lako se suočava s novim situacijama i uvjetima
  • Pokazuje spremnost pomagati kolegama i kupcima, ako je neophodno
  • Može podrediti pojedinačne interese općim, ako to zahtjeva konkretna situacija
  • Preuzima inicijativu u izvođenju preuzetih / danih zadatka

Društvenost

  • Brzo nalazi pristup drugim ljudima
  • Lako ostvaruje kontakt s drugim ljudima
  • Pokazuje interes za kupca
  • Uspijeva održati kontakt u konfliktnim situacijama
  • Otvoren je u razgovoru
  • Pokazuje interes o privatnom životu kupca
  • Dobro komunicira verbalno i neverbalno / govor tijela, gestikulacije
  • Izražava se jasno i razumljivo
  • Koristi više mogućnosti kako bi bio prihvaćen od drugih
  • Slobodno pokazuje radoznalost u komunikaciji

Empatija (osjećaj za poziciju u stavove drugih)

  • Može prepoznati osjećanja kupca i koristiti ih u razgovoru
  • Može odgovoriti na argumente drugih
  • Odgovara na zahtjeve i želje kupca
  • Pokazuje razumijevanje za situaciju u kojoj se kupac nalazi
  • Uvažava kupca
  • Glasnost i držanje tijela je odgovarajuće
  • Svjestan je utiska koji ostavlja na kupca i lako se prilagođava u skladu s tim

Stabilnost u teškim situacijama

  • Ostaje na svom zadatku čak i u najtežim situacijama (kada je poražen)
  • Otvoren je, objektivan i pokazuje razumno ponašanje u konfliktnim situacijama
  • Zadržava pozitivan stav čak i za vrijeme dugih perioda bez uspjeha
  • Pokazuje duhovitost i dosjetljivost
  • Ostaje miran i sabran
  • Ima korisna rješenja u razočaravajućim situacijama

Upornost

  • Drži do svog stavova u toku pregovora
  • Brani svoje mišljenje i razumno / konstruktivno rješava prigovore drugih

Orijentacija prema cilju

  • Postavlja svoje osobne ciljeve i veoma im je posvećen
  • Uporan je u ostvarenju svojih ciljeva
  • Vodi pregovore metodom orijentacije prema cilju
  • Postavlja pitanja orijentirana prema cilju i drži se teme razgovora
  • Određuje prioritete kako bi ostvario cilj

POGODNOST

The average of the category rows. It is used as the Intervju grade on the results page, averaged across all board members.

Figma spells the last category “Orjentacija prema cilju”. The spec and the other forms use “Orijentacija”.

The grading scale is shown at the bottom of the form.

Status of the form

The form is Zaključeno for the current board member and candidate when every category has at least one sub-criterion filled in. Otherwise it is Nije zaključeno.

The current candidate, then “SPREMI I ZATVORI” and “SPREMI”. See Assessment forms.

6.12 Bilješke – Grupna diskusija

A read-only page (trainer only) that collects every board member's notes from the Bilješke section of the Grupna diskusija form. Candidates are collapsible rows, collapsed by default. Inside each row, each board member's note about that candidate is listed.

This page is only accessible to the Trainer role. It is opened from the “Grupna diskusija – Bilješke” link card on the assessment detail page.

Breadcrumb: Assessments > {assessment name}. A “Natrag” link returns to the assessment detail page.

Header:

  • Title “Grupna diskusija – Bilješke”
  • Subtitle: brand · date · assessment name

Content

The page collects in one view the notes every board member wrote in the Bilješke section of the Grupna diskusija form.

  • Each candidate is a collapsible row with an avatar (initials) and their name.
  • All candidates are collapsed by default.
  • An expanded candidate shows one line per board member:
    • the board member’s name (left)
    • the note that board member wrote about this candidate (right)
  • If a board member has not written a note for the candidate, “Nema bilješki.” is shown in grey.
Figma shows all candidates expanded so their content is visible. Per the spec, they are collapsed by default.

This is a read-only overview. Notes cannot be edited on this page.

6.13 Bilješke – Predstavljanje kandidata

A read-only page (trainer only) that collects every board member's notes from the Predstavljanje kandidata form. Candidates are collapsible rows, collapsed by default. Inside each row, all 5 questions are listed, each followed by every board member's note.

This page is only accessible to the Trainer role. It is opened from the “Predstavljanje kandidata – Bilješke” link card on the assessment detail page.

Breadcrumb: Assessments > {assessment name}. A “Natrag” link returns to the assessment detail page.

Header:

  • Title “Predstavljanje kandidata – Bilješke”
  • Subtitle: brand · date · assessment name

Content

The page collects in one view the notes every board member wrote in the Predstavljanje kandidata form.

  • Each candidate is a collapsible row with an avatar (initials) and their name.
  • All candidates are collapsed by default.
  • An expanded candidate shows all 5 questions as numbered grey sub-headers:
    1. Predstavite se:
    2. Gdje ste i što ste do sada radili?
    3. Što Vas je motiviralo da se prijavite na ovaj Oglas?
    4. Kako Vi vidite tržište automobila u Hrvatskoj i posebno (navesti određeno tržišno područje)?
    5. Možete li nam reći nekoliko rečenica o tome koji su to glavni dijelovi koji čine automobil?
  • Under each question, there is one line per board member:
    • the board member’s name, with “Član komisije” under it (left)
    • the note that board member wrote for this question (right)
  • If a board member has not written a note for the question, “Nema bilješki.” is shown in grey.
The question text should be the same as in the form. Figma uses a slightly different wording for question 2 on this page (“Gdje ste i što radili do sada?”).

This is a read-only overview. Notes cannot be edited on this page.

6.14 Assessment results

The "Rezultati assessmenta" page. A results table (activities × candidates) shows each activity's POGODNOST averaged across board members, plus a Pismena provjera znanja row the trainer enters manually and the calculated Zbroj and UKUPNO rows. It also has CSV export and a "Pregled unosa članova komisije" table with each board member's Zaključeno / Nije zaključeno status per activity.

The results page shows a summary of grades for all candidates across all activities. It is opened from the “Rezultati assessmenta” link on the assessment detail page and is visible to both Trainer and Assessment evaluator.

Breadcrumb: Assessments > {assessment name}. A “Natrag” link returns to the assessment detail page.

Header:

  • Title “Rezultati assessmenta”
  • Subtitle: brand · date · assessment name
  • “IZVEZI U CSV” button – exports the results to CSV. Available at any time.

Results table

Activities are rows and candidates are columns. The first column header is “Aktivnost / Kandidat”.

RowSource
Grupna diskusijaApp – average POGODNOST across board members
Igra uloga – prvi kontaktApp – average POGODNOST across board members
Igra uloga – argumentacijaApp – average POGODNOST across board members. If argumentacija is not enabled for the assessment, the prvi kontakt value is copied.
Pismena provjera znanjaEntered manually by the trainer
Zbroj (grey row)Average of the four rows above
IntervjuApp – average POGODNOST of Strukturirani intervju – ocijene across board members
UKUPNO (bold, grey row)Average of Zbroj and Intervju

Calculation rules

  • For the forms in the app, each value is the average across all board members who have submitted grades for that activity and candidate. The POGODNOST (final) row of each form is always used.
  • If no board member has submitted grades for a candidate yet, the cell shows “—”.
  • Zbroj and UKUPNO are calculated even when not all grades have been entered yet, using the values that exist.
  • Values are shown with one decimal and a decimal comma (e.g. 2,7).
In Figma, Zbroj shows “—” for some candidates even though some of their rows have values. That contradicts the spec rule that Zbroj is “calculated even if not all grades are yet inputted”. These docs follow the spec.
Not specified: are Zbroj and UKUPNO calculated from unrounded values or from the rounded one-decimal values shown? Recommended: calculate from unrounded values and round only for display.

Below the table, the grading scale is shown: “Pogodnost: 0 = Nepogodan, 1 = Ne, sa rezervom, 2 = Da, sa rezervom, 3 = Da”.

Editing Pismena provjera znanja

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3911-8327

  • The Pismena provjera znanja row has an edit (pencil) icon at the end of the row. It is visible to the Trainer only.
  • Clicking it turns the row’s cells into input fields, one per candidate, and the icon becomes a “SPREMI” button.
  • Clicking “SPREMI” saves the values and returns the row to read-only mode. Zbroj and UKUPNO are recalculated.
  • An empty value is shown as “—”.
Not specified: the allowed range and format for Pismena provjera znanja. Figma uses values such as 2,5 and 2,8, which suggests the same 0–3 scale with one decimal as the other grades. Please confirm.

Pregled unosa članova komisije

Below the results table, a second card shows submission status. Activities are rows and board members are columns. The first column header is “Aktivnost / Član komisije”.

Each cell shows one of:

  • Zaključeno – green, with a check icon
  • Nije zaključeno – grey, with a clock icon

The status is for that combination of activity and board member. If the activity is not finished for any one of the candidates, the cell shows Nije zaključeno. When a form counts as Zaključeno is defined on each form’s page.

Rows shown in Figma:

  • Grupna diskusija
  • Igra uloga – prvi kontakt
  • Igra uloga – argumentacija
  • Strukturirani intervju – ocijene
The spec says “rows for each activity”, but Figma only shows the graded activities. Predstavljanje kandidata and Strukturirani intervju – bilješke are not shown. Also not specified: whether the argumentacija row is hidden when argumentacija is not enabled, since in that case it would always show Nije zaključeno. Please confirm.

6.15 Backend: Adding new assessment

Backend task for creating an assessment (POST /assessments) – check the API spec and DB structure, set up roles, permissions, tables and lookups, implement, and verify against the spec.

Docs: Adding new assessment Spec: POST /assessments (createAssessment), AssessmentNewRequest, Assessment in api-docs/openapi.yaml DB: Assessments, AssessmentCandidates, AssessmentBoardMembers in api/database/create_schema.sql

This is the first assessment endpoint, so it also includes the shared setup: roles, permissions, tables and the Komisija / Partner lookups.

1. Double check API spec and DB structure

  • Every docs field is in AssessmentNewRequest, with the right required/optional setting: Marka (multiselect → brandCodes), Partner, Naziv, Datum, the “Igra uloga – argumentacija” checkbox, candidates (Ime i prezime required, E-mail optional), Komisija.
  • Limits: 1–5 candidates, 1–10 board members. Check that minItems / maxItems are in both the spec and the docs.
  • Naziv is autofilled from the partner. Is the year appended (“Auto Zubak 2026”)? This is an open question in the docs and only affects the frontend, as long as name stays free text.
  • Default activities: the docs say they are created on save. The schema has no activity rows (activities are fixed in code). Make sure the spec says this.
  • Is 201 + Assessment enough for the next step (back to the list with a toast)?
  • Each DB column has a request field or a value set by the server (CreatedBy, CreatedAt). Decide whether CreatedBy stores the user id or the display name; the spec example is a name.
  • Column types match the spec: BrandCodes VARCHAR(6) vs ^[SACVUL]{1,6}$; Email NVARCHAR(255) NULL.
  • DB constraints: the maximums are capped in the schema by Slot: UQ_AssessmentCandidates_Slot (1–5) and UX_AssessmentBoardMembers_Slot (1–10, active members only). One row per user per assessment is guaranteed by UQ_AssessmentBoardMembers_User. The minimums (1 candidate, 1 member) are checked in code only. Slot is not the display order (that’s CreatedAt).
  • Roles and permissions: the spec uses assessments.edit here. Neither the assessments.* permissions nor the Trainer / Assessment evaluator roles exist in api/Authorization/Permissions.cs / RoleCode.cs. Confirm the role → permission mapping (expected: Trainer gets all assessments.*, Assessment evaluator only assessments.access) and whether Admin gets them too.
  • Komisija lookup: each dropdown option is “Ime i prezime – E-mail – Role”, limited to Trainer and Assessment evaluator users. The spec has no endpoint for this and api/ has no users integration, yet AssessmentBoardMembers.FullName says it is “copied from the users API”. Decide the source and add an endpoint to the spec if the backend serves it.
  • Partner lookup: the docs say Partner is a dropdown and Naziv is autofilled with the partner’s name. Today partnerId is a free-text field (events/invitees), and there is no partner lookup. Decide where the list and the names come from.

2. Implementation

  • Roles and permissions: add Trainer and AssessmentEvaluator to RoleCode (codes, plus the GetRoles catalog). Add the four assessments.* constants to Permissions.cs and map them to roles.
  • DB: apply the -- ASSESSMENTS -- section of create_schema.sql (plus a migration script for existing environments, if used). Add the status helper: active / upcoming / past from Date, using today’s date in Europe/Zagreb.
  • Lookups: if the backend serves them, add the Komisija users endpoint (user id, full name, e-mail, role) and the partner list, and a service that resolves a user id to a full name.
  • Function + repository. Insert the assessment, candidates and board members in one transaction (several writes). Candidates get Slot 1…n and board members 1…m, in request order.
  • Validation → 400: required fields; brandCodes matches the pattern with no duplicate letters; candidate and board member limits; no duplicate boardMemberUserIds; every board member has the Trainer or Assessment evaluator role.
  • Board member FullName comes from the users source.
  • Requires assessments.edit → 403. Response 201 with Assessment (calculated status, candidatesCount).

3. Verify implementation against spec

  • Route, method, request and response shapes match createAssessment.
  • 400 / 401 / 403 / 500 cases can be produced; no undocumented status codes.
  • Evaluator token → 403. Six candidates → 400. Duplicate board member → 400.
  • GET /me/permissions returns the expected set for a Trainer and for an Assessment evaluator. Permission names in Permissions.cs match the PermissionName enum in openapi.yaml.
  • Any new lookup endpoints are in openapi.yaml.

Done when

A trainer can create an assessment that then shows up on the list and on the detail page.

6.16 Backend: Assessment list

Backend task for the assessment list (GET /assessments) – check the API spec and DB structure, permissions and visibility, implement filters, sorting and pagination, and verify against the spec.

Docs: Assessment list Spec: GET /assessments (getAssessments), Assessment, AssessmentStatus in api-docs/openapi.yaml DB: Assessments, AssessmentCandidates (search), AssessmentBoardMembers (visibility) in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Every list column in the docs is in Assessment (name, brand, partner, date, status, candidates count, created by…).
  • Filters match the docs: search by name and candidate name, date range, brand (MARKA), status (PRIKAŽI). Check the parameter formats (dateRange as start/end).
  • Sorting: active → upcoming → past, then date ascending. Check the docs agree.
  • Pagination: the docs say the page size isn’t specified (Figma shows pagination). Set a default perPage in the spec.
  • Do the actions menu rules (edit only for active/upcoming, delete only for upcoming) need any flags in the response, or does the frontend work them out from status?
  • Roles and permissions: list = assessments.access; seeing all assessments = assessments.access-all (Trainer). Assessment evaluators see only assessments they are an active board member of. Check this against the docs permission table, and decide what a removed board member sees (expected: the assessment disappears).
  • DB: status is not stored, so it has to be filtered as a date condition. Check the indexes support the query (IX_Assessments_Date, IX_AssessmentBoardMembers_UserId). Assessments are soft-deleted (see Backend: Delete assessment), so the list must leave out rows where DeletedAt is set.

2. Implementation

  • Roles and permissions: use assessments.access and assessments.access-all, plus the shared “is the caller an active board member” check (UserId = caller and DeletedAt IS NULL). Add the check if it doesn’t exist yet.
  • DB: turn status into a date condition using today’s date in Europe/Zagreb (the same helper as the calculated status).
  • Search: case-insensitive, matching Assessments.Name or any candidate’s FullName. No duplicate rows when several candidates match.
  • Brand filter: brandCode contained in BrandCodes.
  • Sorting and pagination (Pagination, as for other lists).

3. Verify implementation against spec

  • Query parameter names, the response envelope and the item shape match getAssessments.
  • Trainer sees all; evaluator sees only assessments they are a member of; a removed member no longer sees the assessment.
  • The status filter is correct around midnight Europe/Zagreb.

Done when

The list works with every filter, sort and pagination option from the docs, for both roles.

6.17 Backend: Delete assessment

Backend task for deleting an assessment (DELETE /assessments/{assessmentId}) – soft delete, check permissions and status rules, implement, and verify against the spec.

Docs: Assessment list (Actions menu → Ukloni assessment, Delete confirmation modal) Spec: DELETE /assessments/{assessmentId} (deleteAssessment), AssessmentConflict in api-docs/openapi.yaml DB: Assessments (DeletedAt) in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Soft delete (decided): the spec soft-deletes (sets DeletedAt) and keeps candidates, board members, grades and notes. Data is kept for 2 years (see Data retention). BTC školovanje and BTC certificiranje use the same approach.
  • Rule: only upcoming assessments can be deleted → 409 assessment_not_upcoming. Check the docs agree.
  • After deleting, every assessment endpoint (detail, edit, forms, notes overviews, results) → 404, and the list excludes it.
  • Roles and permissions: Trainer only (assessments.edit). Check against the docs permission table.

2. Implementation

  • Roles and permissions: require assessments.edit → 403.
  • DB: set DeletedAt (and UpdatedAt). Make sure the shared “load assessment” helper used by the other endpoints filters DeletedAt IS NULL.
  • Function + repository. Status check → 409. Unknown or already deleted id → 404. Success → 204.

3. Verify implementation against spec

  • Status codes and the error value match deleteAssessment.
  • Active / past assessment → 409. Evaluator → 403. Deleting twice → 404 the second time.
  • After deleting: list excludes it; detail, forms and results → 404; the rows are still in the database.

Done when

A trainer can delete an upcoming assessment, and it disappears everywhere in the app while its data stays in the database.

6.18 Backend: Assessment detail

Backend task for the assessment detail (GET /assessments/{assessmentId}) – check the API spec and DB structure, access rules per role, the caller's activity statuses, implement, and verify against the spec.

Docs: Assessment detail Spec: GET /assessments/{assessmentId} (getAssessmentDetail), AssessmentDetail, AssessmentActivityStatus in api-docs/openapi.yaml DB: all assessment tables in api/database/create_schema.sql (statuses are calculated from AssessmentGrades / AssessmentNotes)

1. Double check API spec and DB structure

  • Header fields in the docs (name, status, brand, date, komisija list) are in AssessmentDetail.
  • Zajedničke aktivnosti → top-level myActivities; Aktivnosti po kandidatu → candidates[].myActivities. Check that the activity lists match the docs, including when argumentation is disabled and when the caller is not a Trainer (no interview-notes).
  • The candidate row shows “email · partner”. The partner is on the assessment, not on the candidate. Confirm.
  • Does the frontend need canEdit / canOpenForms flags, or can it work them out from status + permissions + myActivities?
  • Roles and permissions: access = assessments.access-all, or an active board member. interview-notes only with assessments.notes-access. Decide:
    • a Trainer not in the komisija: read-only (myActivities is null, no “Otvori”), as the docs recommend?
    • an evaluator who isn’t a member of this assessment: 403 or 404? Write both decisions into the spec.
  • DB: boardMembers lists only active members (DeletedAt IS NULL). Statuses are not stored. They are calculated from the caller’s rows in AssessmentGrades / AssessmentNotes.

2. Implementation

  • Roles and permissions: use the shared “active board member” check and the assessments.* permissions.
  • DB: load the assessment, active board members, and candidates (incl. writtenTestGrade from AssessmentWrittenTestGrades, left join) together with the caller’s grades and notes in as few queries as possible.
  • If the caller is an active board member, calculate their Zaključeno status per activity with each form’s status rule (see the Backend: Form – … tasks).
  • argumentation only when hasArgumentation. interview-notes only with assessments.notes-access.
  • Unknown id → 404 AssessmentNotFound.

3. Verify implementation against spec

  • The response matches AssessmentDetail (nullability of both myActivities).
  • Checked with: a Trainer in the komisija, a Trainer outside it, an evaluator in it, an evaluator not in it, and a removed member.

Done when

The detail page can be fully rendered for every role, from this response only.

6.19 Backend: Editing existing assessment

Backend task for editing an assessment (POST /assessments/{assessmentId}) – check the API spec and DB structure, the candidate, board member and argumentacija rules, implement, and verify against the spec.

Docs: Editing existing assessment Spec: POST /assessments/{assessmentId} (updateAssessment), AssessmentNewRequest, AssessmentConflict in api-docs/openapi.yaml DB: Assessments, AssessmentCandidates, AssessmentWrittenTestGrades, AssessmentBoardMembers, AssessmentGrades, AssessmentNotes in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Same fields and validation as create. Candidates have an optional id in the request for matching.
  • Past assessment → 409 assessment_past. Check the docs (edit only for active/upcoming).
  • Blocking candidate removal: the spec blocks it when the candidate has grades, notes or a written test grade (candidate_has_grades). The docs only mention grades and leave notes as an open question. Confirm. The FKs block on all three; if notes shouldn’t block, the edit deletes the removed candidates’ notes before deleting the candidates.
  • Turning off argumentation deletes its grades. The docs recommend a confirmation in the frontend. Does the frontend need to be told whether argumentation grades exist?
  • After saving, the frontend goes to the detail page. Is 200 + Assessment enough?
  • Roles and permissions: Trainer only (assessments.edit), as in the docs permission table.
  • DB: removed board members are soft-deleted (DeletedAt, Slot NULL) and their grades stay. Re-adding a member restores the same row. UQ_AssessmentBoardMembers_User (not filtered) keeps it to one row per user per assessment, so a re-added member can’t be inserted twice.
  • DB: “candidate has data” is enforced by the FKs from AssessmentGrades, AssessmentNotes and AssessmentWrittenTestGrades: deleting such a candidate fails with 547. The maximums are capped by Slot (see create).
  • DB: FK_AssessmentGrades_Argumentation makes HasArgumentation = 0 fail while argumentation rows exist (see Backend: Form – Igranje uloga – argumentacija).
  • Komisija lookup: same source as create, for the dropdown and for FullName of newly added members.

2. Implementation

  • Roles and permissions: require assessments.edit → 403.
  • Lookups: new board members get FullName from the users source; every member must have the Trainer or Assessment evaluator role.

All in one transaction (several writes), in this order, committed only when every statement succeeds. Don’t check for data before deleting: run the write and map the error, so a grade saved at the same moment can’t slip through.

  • Status check → 409 assessment_past.
  • Candidates left out: one DELETE … WHERE AssessmentId = @id AND Id NOT IN (…). FK error 547 → roll back, 409 candidate_has_grades (nothing deleted).
  • Candidates: no id → insert with the lowest free Slot; id that isn’t a candidate of this assessment → 404; others → update name and e-mail.
  • Board members: those left out → set DeletedAt, Slot NULL (first, so their slots are free); re-added → clear DeletedAt, take the lowest free Slot; new → insert with the lowest free Slot.
  • hasArgumentation true → false: delete all argumentation rows in AssessmentGrades for this assessment, then update HasArgumentation. If the update fails with 547, a board member saved argumentation between the two statements: roll back (nothing is saved, the deleted rows come back) and run the whole edit again, as below.
  • Set UpdatedAt. Same validation as create.
  • Concurrent changes: a unique violation (2601 / 2627) on a Slot or UserId (another edit ran at the same time) or the 547 above → roll back and run the whole edit again once, in a new transaction. If it fails the same way again → 500.

3. Verify implementation against spec

  • Status codes and error values match updateAssessment.
  • Scenarios: remove a candidate who has no data (ok), remove one who has grades (409), remove one who has only notes (as decided); remove a board member and re-add them (their grades come back); turn argumentation off and on again (old grades are gone); remove a candidate who only has a written test grade (409); swap a board member for a new one while the komisija is full (10 members).

Done when

All the edit rules in the docs are enforced by the API.

6.20 Backend: Assessment forms

Backend task for the shared form endpoint (GET/POST /assessments/{assessmentId}/forms/{activity}) – check the API spec and DB structure, access rules, the replace-on-save behaviour, implement, and verify against the spec.

Docs: Assessment forms Spec: GET / POST /assessments/{assessmentId}/forms/{activity} (getAssessmentForm, saveAssessmentForm), AssessmentForm, AssessmentFormSaveRequest, AssessmentFormGrade, AssessmentFormNote in api-docs/openapi.yaml DB: AssessmentGrades, AssessmentNotes in api/database/create_schema.sql

The one endpoint pair that every form uses. Each form’s own rules (keys, status) are in the Backend: Form – … tasks.

1. Double check API spec and DB structure

  • Data visibility: GET returns only the caller’s grades and notes. Check against “Data visibility” in the docs.
  • Shared forms (group-discussion, first-contact, argumentation) cover all candidates and take no candidateId. Individual forms (presentation, interview-notes, interview-grades) require candidateId. Check the AssessmentCandidateIdQuery description.
  • Save means replace: the submitted body is the full state, and anything left out is cleared. This works for “Spremi” and “Spremi i zatvori”. For interview-notes, all 6 pages are sent together as one state.
  • Forms can be saved whatever the assessment’s status is (docs open question: upcoming / past). Confirm.
  • The criterion key format "<categoryKey>.<n>" is described, but the keys aren’t listed anywhere. Each form task defines them. Decide whether they go into the spec (an enum or a description per activity) or are only agreed with the frontend. Fix the inconsistent examples (zadatak.2 vs pojava-ponasanje.1).
  • Field key format: notes, q1–q5, p<page>.<n>. Check against the Grupna diskusija, Predstavljanje kandidata and Strukturirani intervju – bilješke tasks.
  • Error cases: not an active board member → 403; interview-notes without notes-access → 403; argumentation disabled → 409 activity_disabled; missing candidateId → 400; a candidateId (query or body) that isn’t a candidate of this assessment → 404 (AssessmentNotFound), the same rule as every other assessment / BTC endpoint.
  • Roles and permissions: only active board members can open or save forms (a Trainer outside the komisija can’t). interview-notes also needs assessments.notes-access. Check against the docs permission table on Assessment detail.
  • DB: the Activity check constraints match the spec. AssessmentGrades allows group-discussion, first-contact, argumentation and interview-grades. AssessmentNotes allows group-discussion, presentation and interview-notes. Grade DECIMAL(2,1) 0–3 matches AssessmentGrade (multipleOf: 0.1). UQ_AssessmentGrades / UQ_AssessmentNotes keep one row per field even when the same form is saved twice at once. AssessmentGrades has AssessmentId (filled from the route) and ArgumentationFlag (1 on argumentation rows, otherwise NULL; see Backend: Form – Igranje uloga – argumentacija).

2. Implementation

  • Roles and permissions: active board member check → 403; assessments.notes-access for interview-notes → 403.
  • DB / catalog: a catalog of activities in code (shared vs individual, graded vs notes). Each form task adds its keys to it. Calculation helpers: category = the average of the entered sub-criteria; POGODNOST = the average of the categories that have a value (empty categories ignored; confirm the open question in the docs); unrounded values.
  • Function + repository for GET and POST, driven by the catalog.
  • POST without a transaction: one MERGE per table (AssessmentGrades, AssessmentNotes), with the target limited to the caller’s rows for (activity, these candidates) and the submitted rows as source: update matches, insert new ones, delete the target rows not in the source. Group discussion has both grades and notes; there the two MERGEs run in one transaction. Empty strings count as “not filled in” and aren’t stored.
  • Errors from the write: unique violation (2601 / 2627, the same form saved twice at once) → retry once; FK error 547 → the candidate was just removed (404) or argumentation was just disabled (409 activity_disabled).
  • Validation: keys exist in the catalog for that activity; grades only on graded forms, notes only on notes forms (group-discussion has both); grade 0–3 with at most one decimal; every candidateId belongs to this assessment (else 404).
  • The response contains the saved form and its recalculated isCompleted.

3. Verify implementation against spec

  • Request and response shapes and status codes match both operations.
  • Two board members save the same form, and neither sees the other’s input.
  • Clearing a field in the body deletes the stored value.

Done when

Any form can be loaded and saved through this endpoint pair. Each form’s own rules are added through the form tasks.

6.21 Backend: Form – Grupna diskusija

Backend task for the Grupna diskusija form (activity group-discussion) – check the criterion keys, notes and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Grupna diskusija Spec: activity group-discussion on /assessments/{assessmentId}/forms/{activity} in api-docs/openapi.yaml DB: AssessmentGrades (group-discussion), AssessmentNotes (group-discussion, fieldKey notes) in api/database/create_schema.sql

A shared form: all candidates in one table, plus one Bilješke note per candidate.

1. Double check API spec and DB structure

  • Criterion keys for the 5 categories / 12 sub-criteria in the docs:
    • Pojava / ponašanje – 3
    • Društvenost / Empatija – 2
    • Govorne sposobnosti – 3
    • Sposobnost uvjeravanja / Upornost – 2
    • Posvećenost / samo-motiviranost – 2
  • Agree on the category keys (e.g. pojava-ponasanje, drustvenost-empatija, …) and add them to the spec or the shared contract (see Backend: Assessment forms).
  • Bilješke: one optional note per candidate, fieldKey = "notes". It doesn’t affect the status. Check the spec description.
  • Status rule: Zaključeno when, for every candidate, every category has at least one sub-criterion filled in.
  • Roles and permissions: Trainer and Assessment evaluator, if they are active board members.
  • DB: both check constraints allow group-discussion (grades and notes).

2. Implementation

  • Roles and permissions: handled by the shared form endpoint. No extra checks.
  • DB / catalog: add the group-discussion criteria and the notes field to the catalog.
  • Status helper for this form (used by detail, forms and results).
  • POGODNOST per candidate = the average of the categories.

3. Verify implementation against spec

  • Every category filled in for all candidates → isCompleted: true; one candidate missing a category → false.
  • Notes are stored and returned, and don’t change the status.
  • Unknown criterion key → 400.

Done when

Grupna diskusija can be filled in and saved, and its status and POGODNOST are correct.

6.22 Backend: Form – Igranje uloga – prvi kontakt

Backend task for the Igranje uloga – prvi kontakt form (activity first-contact) – check the criterion keys and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Igranje uloga – prvi kontakt Spec: activity first-contact on /assessments/{assessmentId}/forms/{activity} in api-docs/openapi.yaml DB: AssessmentGrades (first-contact) in api/database/create_schema.sql

A shared grading form. Its grades are also used for argumentacija when argumentacija is disabled (see Backend: Assessment results).

1. Double check API spec and DB structure

  • Criterion keys for the 7 categories / 16 sub-criteria in the docs:
    • Zadatak – 1
    • Orijentacija prema kupcu – 2
    • Društvenost / Empatija – 3
    • Posvećenost / samo-motiviranost – 2
    • Pojava / ponašanje – 2
    • Tehnika i sposobnost – 3
    • Pripreme i razgovor za ocjenjivanje – 3
  • Agree on the category keys and add them to the spec or the shared contract.
  • Status rule: Zaključeno when, for every candidate, every category has at least one sub-criterion filled in.
  • “Kandidat dobro poznaje Škoda proizvode”: if the brand name is filled in dynamically (docs open question), it’s only a frontend label and the key stays the same. Confirm.
  • Roles and permissions: Trainer and Assessment evaluator, if they are active board members.
  • DB: no notes on this form. The AssessmentNotes check constraint doesn’t allow first-contact. Make sure the spec doesn’t suggest notes here either.

2. Implementation

  • Roles and permissions: handled by the shared form endpoint. No extra checks.
  • DB / catalog: add the first-contact criteria to the catalog.
  • Status helper for this form.

3. Verify implementation against spec

  • Status true / false cases, as for Grupna diskusija.
  • Notes sent for first-contact → 400.

Done when

Prvi kontakt can be filled in and saved, and its status and POGODNOST are correct.

6.23 Backend: Form – Igranje uloga – argumentacija

Backend task for the optional Igranje uloga – argumentacija form (activity argumentation) – check the criterion keys, the enabled/disabled behaviour and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Igranje uloga – argumentacija Spec: activity argumentation on /assessments/{assessmentId}/forms/{activity}, Assessment.hasArgumentation, error activity_disabled in api-docs/openapi.yaml DB: AssessmentGrades (argumentation), Assessments.HasArgumentation in api/database/create_schema.sql

An optional shared grading form, only available when hasArgumentation is true.

1. Double check API spec and DB structure

  • Criterion keys for the 7 categories / 16 sub-criteria in the docs:

    • Zadatak – 1
    • Orijentacija prema kupcu – 4
    • Društvenost / Empatija – 2
    • Vještina govora i izražavanja – 2
    • Posvećenost / samo-motiviranost – 1
    • Tehnika i sposobnost – 3
    • Pripreme i razgovor za ocjenjivanje – 3
  • Category keys that also appear in prvi kontakt (e.g. Zadatak) can share a key name, because keys are scoped per activity. Agree on this with the frontend.

  • Disabled → GET / POST return 409 activity_disabled; detail and results leave it out (or copy prvi kontakt on the results page). The docs also say “always Nije zaključeno” when disabled. Check this doesn’t conflict with leaving it out of the detail page.

  • Turning it off during an assessment deletes its grades (see Backend: Editing existing assessment).

  • Roles and permissions: Trainer and Assessment evaluator, if they are active board members.

  • DB: HasArgumentation BIT NOT NULL DEFAULT 0 matches hasArgumentation (required in the request).

  • DB structure – argumentation rows can’t exist while disabled: check the schema has

    • UQ_Assessments_Argumentation UNIQUE (Id, HasArgumentation) on Assessments
    • AssessmentGrades.ArgumentationFlag BIT NULL with CK_AssessmentGrades_ArgumentationFlag (1 when Activity = 'argumentation', otherwise NULL)
    • FK_AssessmentGrades_Argumentation (AssessmentId, ArgumentationFlag) → Assessments(Id, HasArgumentation)
    • IX_AssessmentGrades_AssessmentId (AssessmentId, ArgumentationFlag), so the FK check on a HasArgumentation update doesn’t scan the table

    Effect: saving an argumentation grade while it’s disabled fails with 547, and setting HasArgumentation = 0 fails with 547 while argumentation rows exist. This closes the race between a board member saving the form and a trainer turning it off, without a lock.

2. Implementation

  • Roles and permissions: handled by the shared form endpoint. No extra checks.
  • DB / catalog: add the argumentation criteria to the catalog. Check HasArgumentation in the forms endpoint → 409. Insert argumentation rows with ArgumentationFlag = 1. A 547 on save (disabled in the meantime) → 409 activity_disabled.
  • Status helper for this form (always false when disabled).

3. Verify implementation against spec

  • Disabled assessment → 409 on GET and POST.
  • Directly in the DB: inserting an argumentation row for a disabled assessment fails; UPDATE Assessments SET HasArgumentation = 0 fails while argumentation rows exist.
  • Enabled → status true / false cases, as for Grupna diskusija.

Done when

Argumentacija works when enabled and is blocked when disabled.

6.24 Backend: Form – Predstavljanje kandidata

Backend task for the Predstavljanje kandidata notes form (activity presentation) – check the field keys and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Predstavljanje kandidata Spec: activity presentation on /assessments/{assessmentId}/forms/{activity} (candidateId required) in api-docs/openapi.yaml DB: AssessmentNotes (presentation, fieldKey q1–q5) in api/database/create_schema.sql

An individual notes form with 5 open questions and no grades.

1. Double check API spec and DB structure

  • Field keys q1–q5 match the 5 questions in the docs, in order.
  • Status rule: Zaključeno when all 5 fields are filled in (for this board member and candidate).
  • Notes from all board members feed Bilješke – Predstavljanje kandidata.
  • Roles and permissions: Trainer and Assessment evaluator, if they are active board members.
  • DB: no grades. The AssessmentGrades check constraint doesn’t allow presentation; the spec should reject grades too. Note NVARCHAR(MAX) — is a max length needed in the spec?

2. Implementation

  • Roles and permissions: handled by the shared form endpoint. No extra checks.
  • DB / catalog: add the presentation fields to the catalog.
  • Status helper for this form. A whitespace-only note counts as empty.

3. Verify implementation against spec

  • Missing candidateId → 400.
  • 4 fields filled in → false; 5 → true.
  • Grades sent → 400.

Done when

Predstavljanje kandidata can be filled in per candidate, and its status is correct.

6.25 Backend: Form – Strukturirani intervju – bilješke

Backend task for the trainer-only Strukturirani intervju – bilješke notes form (activity interview-notes) – check the field keys, privacy and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Strukturirani intervju – bilješke Spec: activity interview-notes on /assessments/{assessmentId}/forms/{activity} (candidateId required, assessments.notes-access) in api-docs/openapi.yaml DB: AssessmentNotes (interview-notes, fieldKey p<page>.<n>) in api/database/create_schema.sql

An individual notes form for the Trainer only, across 6 pages with 21 fields. The notes are private and not shown anywhere else.

1. Double check API spec and DB structure

  • Field keys for the 21 fields (1 + 5 + 4 + 5 + 5 + 1), page by page:
    • page 1: p1.1
    • page 2: p2.1–p2.5
    • page 3: p3.1–p3.4
    • page 4: p4.1–p4.5
    • page 5: p5.1–p5.5
    • page 6: p6.1 Check the counts against each page in the docs.
  • Privacy: these notes must never appear on the Bilješke endpoint or in the results. Each trainer sees only their own.
  • Moving between pages doesn’t save (docs recommendation). The whole form is saved at once with “Spremi”. Check the spec allows one save with all pages.
  • Status rule: Zaključeno when all 21 fields are filled in.
  • Roles and permissions: requires assessments.notes-access (Trainer) and active board member. An Assessment evaluator gets 403, and the activity is hidden from them on the detail page.
  • DB: the AssessmentNotes check constraint allows interview-notes; FieldKey NVARCHAR(50) fits p<page>.<n>.

2. Implementation

  • Roles and permissions: check assessments.notes-access in the forms endpoint (→ 403) and on the detail page (activity not returned).
  • DB / catalog: add the interview-notes fields to the catalog. The aggregated notes query must never select interview-notes.
  • Status helper for this form.

3. Verify implementation against spec

  • Evaluator token → 403 on GET and POST.
  • 20 fields filled in → false; 21 → true.
  • Trainer A cannot see trainer B’s notes.

Done when

Trainers can fill in their private interview notes, and nobody else can read them.

6.26 Backend: Form – Strukturirani intervju – ocijene

Backend task for the Strukturirani intervju – ocijene grading form (activity interview-grades) – check the criterion keys and status rule against the API spec and DB structure, implement, and verify.

Docs: Form – Strukturirani intervju – ocijene Spec: activity interview-grades on /assessments/{assessmentId}/forms/{activity} (candidateId required) in api-docs/openapi.yaml DB: AssessmentGrades (interview-grades) in api/database/create_schema.sql

An individual grading form (one candidate column). Its POGODNOST is the Intervju grade on the results page.

1. Double check API spec and DB structure

  • Criterion keys for the 6 categories / 36 sub-criteria in the docs:
    • Predanost / Odnos prema preuzetim obavezama – 6
    • Društvenost – 10
    • Empatija (osjećaj za poziciju u stavove drugih) – 7
    • Stabilnost u teškim situacijama – 6
    • Upornost – 2
    • Orijentacija prema cilju – 5
  • Agree on the category keys and add them to the spec or the shared contract. Use “orijentacija” (not Figma’s “orjentacija”) in keys.
  • Status rule: Zaključeno when every category has at least one sub-criterion filled in (for this board member and candidate).
  • Roles and permissions: Trainer and Assessment evaluator, if they are active board members.
  • DB: no notes on this form. The AssessmentNotes check constraint doesn’t allow interview-grades.

2. Implementation

  • Roles and permissions: handled by the shared form endpoint. No extra checks.
  • DB / catalog: add the interview-grades criteria to the catalog.
  • Status helper for this form.

3. Verify implementation against spec

  • Missing candidateId → 400.
  • One category empty → false; all categories have a value → true.
  • Notes sent → 400.

Done when

Strukturirani intervju – ocijene can be filled in per candidate, and its status and POGODNOST are correct.

6.27 Backend: Bilješke – Grupna diskusija

Backend task for the aggregated Grupna diskusija notes (GET /assessments/{assessmentId}/notes/group-discussion) – check the API spec and DB structure, trainer-only access, implement, and verify.

Docs: Bilješke – Grupna diskusija Spec: GET /assessments/{assessmentId}/notes/{activity} (getAssessmentNotes) with group-discussion, AssessmentNotesOverview in api-docs/openapi.yaml DB: AssessmentNotes (group-discussion, fieldKey notes), AssessmentBoardMembers in api/database/create_schema.sql

A read-only, trainer-only page showing every board member’s Grupna diskusija note per candidate.

1. Double check API spec and DB structure

  • Header data in the docs (brand · date · assessment name): does the frontend get it from the detail endpoint, or should it be in AssessmentNotesOverview?
  • Notes from removed board members are included (spec). Check that the docs agree.
  • Board members who haven’t written a note: does the page show them (“Nema bilješki.”)? If yes, the response must list them too, because the spec currently only returns existing notes. Decide and update the spec.
  • Ordering of candidates and board members: define it in the spec.
  • Roles and permissions: requires assessments.notes-access (Trainer only). Decide whether a Trainer outside the komisija can see it (the docs recommend yes).
  • DB: board member names come from AssessmentBoardMembers.FullName, including soft-deleted rows.

2. Implementation

  • Roles and permissions: assessments.notes-access → 403 otherwise. Unknown assessment → 404.
  • DB: join notes with board members (including DeletedAt IS NOT NULL) to get fullName.
  • Function + repository, shared with Backend: Bilješke – Predstavljanje kandidata and parameterised by activity. activity other than group-discussion / presentation → 400.

3. Verify implementation against spec

  • The response matches AssessmentNotesOverview.
  • Evaluator → 403. A removed member’s note is still shown.
  • interview-notes can’t be requested here.

Done when

The trainer sees every board member’s Grupna diskusija notes, grouped by candidate.

6.28 Backend: Bilješke – Predstavljanje kandidata

Backend task for the aggregated Predstavljanje kandidata notes (GET /assessments/{assessmentId}/notes/presentation) – check the API spec and DB structure, trainer-only access, implement, and verify.

Docs: Bilješke – Predstavljanje kandidata Spec: GET /assessments/{assessmentId}/notes/{activity} (getAssessmentNotes) with presentation, AssessmentNotesOverview in api-docs/openapi.yaml DB: AssessmentNotes (presentation, fieldKey q1–q5), AssessmentBoardMembers in api/database/create_schema.sql

A read-only, trainer-only page. For each candidate it shows the 5 questions, and under each question every board member’s note.

1. Double check API spec and DB structure

  • Each docs question maps to its fieldKey (q1–q5). The question text is a frontend label and should be the same as in the form (the docs note that Figma’s wording for question 2 is different).
  • “Nema bilješki.” is shown for each board member who hasn’t answered a question. Same decision as for Grupna diskusija: should the response list every board member for each candidate?
  • Notes from removed board members are included.
  • Roles and permissions: requires assessments.notes-access (Trainer only).
  • DB: board member names come from AssessmentBoardMembers.FullName, including soft-deleted rows.

2. Implementation

  • Roles and permissions: assessments.notes-access → 403 otherwise.
  • DB: reuse the query from Backend: Bilješke – Grupna diskusija with activity = presentation.
  • Order notes by question (q1–q5), then by board member.

3. Verify implementation against spec

  • The response matches AssessmentNotesOverview, with fieldKey values q1–q5.
  • Evaluator → 403.

Done when

The trainer sees all board members’ answers to the 5 questions, for each candidate.

6.29 Backend: Assessment results

Backend task for the results page (GET /assessments/{assessmentId}/results, POST /written-test-grades) – check the calculation rules against the API spec and DB structure, permissions, implement, and verify with a hand-calculated example.

Docs: Assessment results Spec: GET /assessments/{assessmentId}/results (getAssessmentResults), POST /assessments/{assessmentId}/written-test-grades (setAssessmentWrittenTestGrades), AssessmentResults, AssessmentWrittenTestGrade in api-docs/openapi.yaml DB: AssessmentGrades, AssessmentCandidates, AssessmentWrittenTestGrades, AssessmentBoardMembers in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Each results table row in the docs maps to a field: Grupna diskusija → groupDiscussion, prvi kontakt → firstContact, argumentacija → argumentation, Pismena provjera znanja → writtenTest, Zbroj → sum, Intervju → interview, UKUPNO → total.
  • Calculation rules in the spec vs the docs:
    • activity value = the average POGODNOST across board members who entered grades (including removed members); null if there are none
    • argumentation disabled → copy firstContact
    • Zbroj = the average of the non-null group discussion, first contact, argumentation and written test values (calculated even when some are missing; the docs follow the spec, not Figma)
    • UKUPNO = the average of the non-null Zbroj and Intervju
    • unrounded values (docs recommendation). Write this into the spec.
  • Pregled unosa članova komisije: the spec lists only graded activities (argumentation only if enabled; no presentation / interview-notes). The docs say this is an open question. Confirm. An individual activity counts as completed only when it’s completed for every candidate.
  • Header data (brand · date · name): from the detail endpoint, or add it here?
  • CSV export is done in the frontend. Confirm the response has everything the CSV needs.
  • Roles and permissions: results are visible to Trainers (assessments.access-all) and to evaluators in the komisija. Editing the written test grade needs assessments.edit. Check against the docs.
  • DB: AssessmentWrittenTestGrades.Grade DECIMAL(2,1) 0–3, one row per candidate that has a grade (no row = no grade). The docs only guess this range from Figma. Confirm. It’s a separate table so its FK blocks removing a candidate who has a grade. Grades of removed board members stay in AssessmentGrades and are counted.

2. Implementation

  • Roles and permissions: GET → assessments.access-all or an active board member; POST → assessments.edit.
  • DB: one query for all grades of the assessment, then calculate in code with the shared helpers (category → POGODNOST per member and candidate → average across members).
  • Board member overview: active members only, using each form’s status helper (Grupna diskusija, prvi kontakt, argumentacija, Strukturirani intervju – ocijene).
  • POST /written-test-grades: update only the listed candidates (null clears the grade); each candidateId must belong to the assessment (404); grade validated 0–3 with one decimal; return the recalculated results. One MERGE on AssessmentWrittenTestGrades, no transaction: a value inserts or updates the row, null deletes it. FK error 547 (candidate removed in the meantime) → 404; unique violation (saved twice at once) → retry once.

3. Verify implementation against spec

  • Response shapes and status codes match both operations.
  • Results checked against an example calculated by hand: 2 board members (one removed after grading), 3 candidates, some grades missing, argumentation on and off.
  • A candidate with no grades at all → every activity field and sum / total are null.
  • Evaluator → POST 403. Writing a grade of 3.25 or 4 → 400.

Done when

The results page and CSV can be built from the API, and the numbers match the hand-calculated example.

7. BTC

BTC školovanje and BTC certificiranje are part of the “Assessment i školovanje” section of the app (side menu: Assessment i školovanja → BTC školovanje / BTC certificiranje).

BTC školovanje

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3261-14528

BTC školovanje is a structured training program of 4 modules, each lasting 2 days.

The goal is to digitalize attendance tracking and feedback collection during the training.

All users with access to the BTC školovanje section have access to everything in it: viewing, creating, editing and deleting školovanje records, and viewing feedback results.

Pages:

BTC certificiranje

Figma URL: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3261-14888

BTC certificiranje is the final certification step after BTC školovanje. Candidates take two exams:

Candidates are sent to certification by a partner (e.g. Auto Zubak).

The goal is to digitalize oral exam grading and to give a clear overview of certification results across both exams.

All users with access to the BTC certificiranje section have access to everything in it: viewing, creating, editing and deleting certificiranja, and viewing results.

Pages:

Roles

A user can have only one role at a time. Roles are assigned in user management.

Trainer

BTC evaluator

Action in BTC certificiranjeTrainerBTC evaluator
View the list, detail and Rezultati certificiranjaYesYes
Create, edit and delete a certificiranjeYesNo
Enter Modul 5 – Pismeni ispit resultsYesYes
Open/fill Modul 5 – Usmeni ispitYes (komisija members only)Yes (komisija members only)
Per the role definitions, only the Trainer role has access to BTC školovanje. Assessment evaluators and BTC evaluators do not see it. Please confirm.

Data retention

Data is kept for 2 years. Deleting a školovanje or certificiranje only hides it in the app; its data (candidates, attendance, feedback values, signature files, komisija, grades) stays in the database for the retention period.

7.1 Adding new BTC školovanje

The "Kreiraj novo BTC školovanje" form, with the sections Osnovni podaci (Marka, Naziv školovanja, and optional start and end dates for Modul 1–4) and Kandidati (1–20, each with Ime i prezime, E-mail, Partner, Marka). Saving creates 4 modules (2 attendance days each), a Rezultati feedback forme page and a Potpisi page, then returns the user to the list with a success toast.

The user adds a new BTC školovanje by clicking “NOVO ŠKOLOVANJE” on the BTC školovanje list.

Page title: “Kreiraj novo BTC školovanje”, with a back arrow that returns to the list. Breadcrumb: BTC školovanje > Kreiraj novo BTC školovanje.

Osnovni podaci

  • Marka – multiselect dropdown, mandatory
  • Naziv školovanja – text field, mandatory (placeholder “npr. Školovanje 2026/1”)
  • Termini po modulima – one row per module (Modul 1, Modul 2, Modul 3, Modul 4), each with:
    • Datum početka – date picker, optional
    • Datum završetka – date picker, optional
  • Info box under the dates: “Nakon datuma završetka Modula 4, slučaj će biti označen kao “Prošli”.”

When the Modul 4 end date has passed, the školovanje is automatically marked as Prošli.

Figma labels the name field “Naziv školovanje”. The spec uses “Naziv školovanja”, which is grammatically correct.

Within one module, Datum završetka cannot be before Datum početka.

Not specified: must the modules be in date order? For example, can Modul 2 start before Modul 1 ends, or even before Modul 1 starts? Options: no rule (modules can overlap or be in any order); or each module must start after the previous module’s end date. Recommended: each module after the previous one. Please confirm.

Kandidati

Section description: “Dodajte kandidate koji sudjeluju u školovanju (max 20).”

Each candidate row has:

  • Ime i prezime – text field, mandatory (placeholder “npr. Ivan Horvat”)
  • E-mail – text field, optional (placeholder npr. ivan.horvat@primjer.hr)
  • Partner – dropdown, mandatory (placeholder “Odaberi”)
  • Marka (kandidat) – dropdown, mandatory (placeholder “Odaberi”). This is the candidate’s brand, which is different from Marka under Osnovni podaci.

Rules:

  • At least one candidate must be added. A maximum of 20 candidates can be added.
  • The “+ Dodaj polje” button adds another candidate row.
  • Every additional row has a “–” button that removes it.
  • Candidates are stored per školovanje only. They are not linked to a shared candidate database.
Not specified: should the Marka (kandidat) options be limited to the brands selected under Osnovni podaci?

Default structure

When a new BTC školovanje is created, the following are added automatically:

  • Rezultati feedback forme – a link to a table for grades that are entered manually
  • Potpisi – a link to the signature page, with one section per module and no files uploaded
  • Four training modules: Modul 1, Modul 2, Modul 3 and Modul 4. Each module has its feedback form and two training days (Dan 1 and Dan 2) for every candidate.

Saving

  • The “KREIRAJ ŠKOLOVANJE” button in the sticky footer saves the školovanje.
  • Mandatory fields are validated before saving.
  • When the školovanje is created, the user is taken to the BTC školovanje list and a success toast notification is shown.

7.2 BTC školovanje list

List of BTC školovanje programs across all brands (10 per page) with search, date range, brand and status filters, a "Novo školovanje" button, and a per-row actions menu (Detalji / Uredi / Ukloni školovanje) with a delete confirmation modal. Status (Aktivni / Nadolazeći / Prošli) is calculated from the Modul 1 start date and the Modul 4 end date.

The BTC školovanje list displays all training programs across all brands. It is opened from the side menu: Assessment i školovanja → BTC školovanje.

Page title: “BTC školovanje”. Breadcrumb: BTC školovanje.

List columns

  • Naziv – name of the školovanje
  • Marka – brand(s)
  • Datum početka Modul 1 – start date of Modul 1 (dd.mm.yyyy)
  • Status – tag with one of: Aktivni, Nadolazeći, Prošli
  • Actions menu (three dots icon “…”)

Status

Status is set automatically from the module dates:

  • Nadolazeći – the Modul 1 start date is in the future
  • Aktivni – from the Modul 1 start date until the Modul 4 end date. If no Modul 4 end date has been entered, the školovanje stays Aktivni.
  • Prošli – the Modul 4 end date has passed
Module dates are optional. What status should a školovanje have when no Modul 1 start date has been entered? Recommended: Nadolazeći, sorted at the end of its group.

Sorting

The list is sorted by status first (Aktivni → Nadolazeći → Prošli). Within each status group it is sorted by the Modul 1 start date, ascending.

Search and filters

  • Search field “Pretraži po nazivu školovanja ili kandidatu” – searches by školovanje name and by candidate name
  • Date range picker (dd.mm.yyyy - dd.mm.yyyy) – filters by date
  • MARKA – filter by brand
  • PRIKAŽI – filter by status, with the options:
    • All
    • Aktivni
    • Nadolazeći
    • Prošli

By default all statuses are shown.

The date range filter shows every školovanje whose span (Modul 1 start date to Modul 4 end date) overlaps the selected range, even partly. A školovanje without a Modul 4 end date counts as still running.

Pagination

The list is paginated with 10 records per page, the same as the events list (“Prikazuje se 1-10 od 14” and page numbers).

Please confirm 10 records per page. The spec doesn’t give a page size for the BTC školovanje list, and the events docs don’t specify one either. 10 is what the events list uses in the app, and the same as BTC certificiranje.

User actions

  • View all školovanje records
  • Search by školovanje name or candidate name
  • Filter by date, brand (MARKA) and status (PRIKAŽI)
  • Add a new školovanje (“NOVO ŠKOLOVANJE” button) – opens Adding new BTC školovanje
  • View the školovanje detail page (all statuses)
  • Edit a školovanje (Aktivni and Nadolazeći only). Prošli cannot be edited.
  • Delete a školovanje (Nadolazeći only). Aktivni and Prošli cannot be deleted.

Actions menu

Clicking the three dots icon on a row opens a menu. Its options depend on the status:

StatusOptions
AktivniDetalji školovanje, Uredi školovanje
NadolazećiDetalji školovanje, Uredi školovanje, Ukloni školovanje (red, with a trash icon)
ProšliDetalji školovanje

Delete confirmation modal

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=4104-9896

  • Warning icon, title “Ukloni školovanje?”, with the školovanje name as a subtitle (e.g. “Školovanje 2026/1”)
  • Text: “Jeste li sigurni da želite ukloniti ovo školovanje?” (Figma has the typo “ovu”)
  • Buttons:
    • Odustani – closes the modal without deleting
    • Ukloni (red) – deletes the školovanje. It disappears from the app, but its data is kept (see Data retention).
  • Close icon (x) in the top right corner – closes the modal without deleting

7.3 BTC školovanje detail

The BTC školovanje detail page, with a header (name, status, brand), an "Uredi školovanje" button, links to Rezultati feedback forme and Potpisi, and 4 collapsible module sections. Each section shows its date range and an attendance table (Kandidat × Dan 1 / Dan 2). Each day can be edited inline with checkboxes and saved with Spremi.

The BTC školovanje detail page is accessible to all users with access to the BTC školovanje section. It is opened with “Detalji školovanje” from the list.

Breadcrumb: BTC školovanje > {školovanje name}. A “Natrag” link returns to the list.

  • Školovanje name with a status tag (Aktivni / Nadolazeći / Prošli)
  • Brand(s)
  • “UREDI ŠKOLOVANJE” button – opens Uredi BTC školovanje. Only shown when the status is Aktivni or Nadolazeći.

Two link cards are shown under the header:

Modules

The page has 4 collapsible module sections (Modul 1–4).

  • All sections are collapsed by default.
  • The section header shows “Modul X” with a calendar icon and the module’s date range (e.g. “12.05.2025. – 14.05.2025.”). If no dates have been entered for the module, “-” is shown.
  • An expanded section shows the attendance table for that module.
Figma shows Modul 1 expanded so its content is visible. Per the spec, all sections are collapsed by default.

Attendance table

Columns: KANDIDAT, DAN 1, DAN 2.

  • KANDIDAT – avatar (initials) and the candidate’s name
  • DAN 1 / DAN 2 – for each candidate:
    • a green check icon if the candidate was present
    • a grey “–” icon if the candidate was not present (also the default)
  • Each day column header (DAN 1, DAN 2) has an edit (pencil) icon.

Editing attendance

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3560-17259

  1. Clicking the edit icon of a day switches that day’s icons to checkboxes, one per candidate. A checked box means present.
  2. “Odustani” and “Spremi” buttons appear on the right of the table header.
  3. The user checks or unchecks candidates.
  4. “Spremi” saves the selection. The checkboxes turn back into check / “–” icons that reflect the saved values.
  5. “Odustani” discards the changes and returns to view mode.

Only one day is in edit mode at a time. The other day keeps showing icons.

The spec only mentions “Spremi”. The “Odustani” button comes from Figma.
The spec says “each module consists of its feedback form”, but there is no feedback form on the detail page in Figma. Feedback results are entered manually on Rezultati feedback forme. Please confirm that nothing about the feedback form is needed on the detail page.

7.4 Editing existing BTC školovanje

The "Uredi BTC školovanje" form (Aktivni and Nadolazeći only). Same fields as Adding new BTC školovanje, prefilled. The Modul 4 end date can't be moved into the past. Candidates who already have data cannot be removed. Saving returns the user to the BTC školovanje list with a success toast.

The user edits a školovanje either from the actions menu on the BTC školovanje list (“Uredi školovanje”) or with the “UREDI ŠKOLOVANJE” button on the BTC školovanje detail page.

Only školovanje records with status Aktivni or Nadolazeći can be edited.

The form has the same fields and validation as Adding new BTC školovanje, prefilled with the current data, with these differences:

  • Page title is “Uredi BTC školovanje”
  • The CTA button saves the changes
The spec says the CTA is “Uredi školovanje”, but Figma shows “SPREMI ŠKOLOVANJE”. The Figma breadcrumb also still says “Kreiraj novo BTC školovanje”, which should be “Uredi BTC školovanje”. Recommended: “SPREMI ŠKOLOVANJE”, for consistency with “SPREMI ASSESSMENT”.

Rules

  • The Modul 4 end date cannot be moved into the past (before today). An edit therefore can’t make the školovanje Prošli.
  • A candidate who already has any data (e.g. attendance) cannot be removed. The “–” button is not available for that candidate.
  • Adding a candidate adds them to every module’s attendance table (Dan 1 and Dan 2).

Saving

When the školovanje is saved, the user is taken to the BTC školovanje list and a success toast notification is shown.

7.5 Potpisi page

The Potpisi sub-page of a BTC školovanje. One section per module (Modul 1–4) where the user uploads scanned signature lists (jpg, png, pdf …) with a native file upload. Uploaded files are listed as download links and can be deleted.

The Potpisi page is a sub-page of a BTC školovanje record. It is opened from the “Potpisi” link card on the BTC školovanje detail page.

It is the central place to upload the scanned physical signature documents (potpisne liste) collected during the training, organized by module.

Breadcrumb: BTC školovanje > {školovanje name} > Potpisi. A “Natrag” link returns to the školovanje detail page.

Header:

  • Title “Potpisi”
  • Subtitle: the školovanje name

Module sections

The page lists the four training modules (Modul 1–4) as separate cards. Each card shows:

  • The module name
  • An “Uredi” button (pencil icon)
  • An “Odaberi datoteke” button – opens the native file picker
  • A list of the files already uploaded for the module. Each filename is a link that downloads the file (e.g. “Img_23413413432423423424.jpeg”).

Uploading signature documents

  • One or more files can be uploaded per module.
  • Allowed file types: images and PDF (jpg, png, pdf …).
  • A native file upload is used.
  • Uploaded files can be downloaded (click the filename) and deleted (x).
Figma shows both an “Uredi” button and an “Odaberi datoteke” button, but not what “Uredi” does. Recommended: “Uredi” switches the card into edit mode, which shows an x next to each file to delete it. “Odaberi datoteke” uploads immediately. Also not specified: the maximum file size and number of files, and whether deleting asks for confirmation.

7.6 Rezultati feedback forme

The Rezultati feedback forme page of a BTC školovanje. A table with one row per module (Modul, Trener, Ocjena) where the user enters the trainer(s) and the aggregated grade from the anonymous candidate feedback form. Each row is edited inline with an edit icon and saved with SPREMI.

The page is opened from the “Rezultati feedback forme” link card on the BTC školovanje detail page.

Breadcrumb: BTC školovanje > {školovanje name} > Rezultati feedback forme. A “Natrag” link returns to the školovanje detail page.

Header:

  • Title “Rezultati feedback forme”
  • Subtitle: the školovanje name

Results table

One row per module (Modul 1–4), with these columns:

  • Modul – module number (Modul 1–4)
  • Trener – the trainer(s) responsible for the module. Entered manually. If a module has several trainers, they are all written in the same field. Shows “—” if not set.
  • Ocjena – the aggregated grade from the anonymous candidate feedback form for that module. Entered manually. Shows “—” if not set.
  • Edit (pencil) icon at the end of the row

Editing a row

  1. Clicking the edit icon turns the row’s Trener and Ocjena into input fields (Trener – text, Ocjena – number), and the icon becomes a “SPREMI” button.
  2. “SPREMI” saves the values and returns the row to read-only mode.
Not specified: the range and number of decimals allowed for Ocjena. Figma uses values such as 4,23 and 3,82 (two decimals, decimal comma), which suggests a 1–5 scale. Please confirm.

7.7 Adding new BTC certificiranje

The "Kreiraj novo BTC certificiranje" form, with the sections Osnovni podaci (one Marka, Naziv certificiranja, start and end dates), Kandidati (1–20, each with Ime i prezime, E-mail, Partner) and Komisija (1–10, a user dropdown that shows each user's name and e-mail). Saving creates the Rezultati certificiranja page and a Modul 5 – Usmeni ispit activity per candidate, then returns the user to the list with a success toast.

The user adds a new BTC certificiranje by clicking “NOVO CERTIFICIRANJE” on the BTC certificiranje list.

Page title: “Kreiraj novo BTC certificiranje”, with a back arrow that returns to the list.

Osnovni podaci

  • Marka – dropdown, mandatory. Only one brand can be selected.
  • Naziv certificiranja – text field, mandatory (placeholder “npr. Certificiranje 2026/1”)
  • Datum početka certificiranja – date picker, mandatory
  • Datum završetka certificiranja – date picker, mandatory
  • Info box under the dates: “Nakon datuma završetka, slučaj će biti označen kao “Prošli”.”

When the end date has passed, the certificiranje is automatically marked as Prošli.

Recommended validation: Datum završetka cannot be before Datum početka.

Kandidati

Section description: “Dodajte kandidate koji sudjeluju u certificiranju (max 20).”

Each candidate row has:

  • Ime i prezime – text field, mandatory (placeholder “npr. Ivan Horvat”)
  • E-mail – text field, optional (placeholder npr. ivan.horvat@primjer.hr)
  • Partner – dropdown, mandatory (placeholder “Odaberi”)

Rules:

  • At least one candidate must be added. A maximum of 20 candidates can be added.
  • The “+ Dodaj polje” button adds another candidate row.
  • Every additional row has a “–” button that removes it.
  • Candidates are stored per certificiranje only. They are not linked to a shared candidate database.

Komisija

Section description: “Dodajte članove komisije koji provode certificiranje” (Figma has the typo “certificiranju”).

Each row has one field:

  • Ime i prezime – dropdown, mandatory (placeholder “Odaberi”). Each option shows the user’s name and e-mail.

There is no separate e-mail field.

Rules:

  • At least one komisija member must be added. A maximum of 10 komisija members can be added.
  • The “+ Dodaj polje” button adds another member row.
  • Every additional row has a “–” button that removes it.
  • Komisija members must first be added as users in user management. Only then can they be selected here.
Not specified: which users appear in the Ime i prezime dropdown. Recommended: only users with the Trainer or BTC evaluator role, with users already selected in another row excluded.

Default structure

When a new BTC certificiranje is created, the following are added automatically:

  • Rezultati certificiranja – a link to the results table
  • Modul 5 – Usmeni ispit – one per candidate, for each candidate added to the certificiranje

This activity is fixed and cannot be added or removed manually. It is the same for every certificiranje.

Saving

  • The “KREIRAJ CERTIFICIRANJE” button in the sticky footer saves the certificiranje.
  • Mandatory fields are validated before saving.
  • When the certificiranje is created, the user is taken to the BTC certificiranje list and a success toast notification is shown.

7.8 BTC certificiranje list

List of BTC certificiranja across all brands (10 per page) with search, date, brand and status filters, a "Novo certificiranje" button, and a per-row actions menu (Detalji / Uredi / Ukloni certificiranje) with a delete confirmation modal. Status (Aktivni / Nadolazeći / Prošli) is calculated from the start and end dates.

The BTC certificiranje list displays all certification records across all brands. It is opened from the side menu: Assessment i školovanja → BTC certificiranje.

Page title: “BTC certificiranje”. Breadcrumb: BTC certificiranje.

List columns

  • Naziv – name of the certificiranje
  • Marka – brand
  • Datum početka – start date (dd.mm.yyyy)
  • Datum završetka – end date (dd.mm.yyyy)
  • Status – tag with one of: Aktivni, Nadolazeći, Prošli
  • Actions menu (three dots icon “…”)

Status

Status is set automatically from the start and end dates:

  • Nadolazeći – the start date is in the future
  • Aktivni – from the start date until the end date
  • Prošli – the end date has passed

Sorting and pagination

  • The list is sorted by status first (Aktivni → Nadolazeći → Prošli). Within each status group it is sorted by start date, ascending.
  • Pagination: 10 records per page (“Prikazuje se 1-10 od 14” and page numbers).

Search and filters

  • Search field “Pretraži po nazivu certificiranja ili kandidatu” – searches by certificiranje name and by candidate
  • Date filter (dd.mm.yyyy)
  • MARKA – filter by brand
  • PRIKAŽI – filter by status, with the options:
    • All
    • Aktivni
    • Nadolazeći
    • Prošli

By default all statuses are shown.

Figma shows a single-date picker here, but a date range picker on Assessments and BTC školovanje. Recommended: use a date range for consistency, matching certificiranja that overlap the range.

User actions

  • View all certificiranje records
  • Search by certificiranje name or candidate
  • Filter by date, brand (MARKA) and status (PRIKAŽI)
  • Add a new certificiranje (“NOVO CERTIFICIRANJE” button) – opens Adding new BTC certificiranje
  • View the certificiranje detail page (all statuses) – “Detalji certificiranja”
  • Edit a certificiranje (Aktivni and Nadolazeći only) – “Uredi certificiranje”. Prošli cannot be edited.
  • Delete a certificiranje (Nadolazeći only) – “Ukloni certificiranje”. Aktivni and Prošli cannot be deleted.

Creating, editing and deleting are available to the Trainer role only. BTC evaluators see the list and the detail pages, so their actions menu shows only “Detalji certificiranja” and the “NOVO CERTIFICIRANJE” button is hidden. See Roles.

Actions menu

Clicking the three dots icon on a row opens a menu. Its options depend on the status:

StatusOptions
AktivniDetalji certificiranja, Uredi certificiranje
NadolazećiDetalji certificiranja, Uredi certificiranje, Ukloni certificiranje (red, with a trash icon)
ProšliDetalji certificiranja

Figma has the typo “Ukloni certiiciranje” in the menu.

Delete confirmation modal

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=4104-9925

  • Warning icon, title “Ukloni certificiranje?”, with the certificiranje name as a subtitle (e.g. “Certificiranje 2026/1”)
  • Text: “Jeste li sigurni da želite ukloniti ovo certificiranje?” (Figma has the typo “ovu”)
  • Buttons:
    • Odustani – closes the modal without deleting
    • Ukloni (red) – deletes the certificiranje. It disappears from the app, but its data is kept (see Data retention).
  • Close icon (x) in the top right corner – closes the modal without deleting

7.9 BTC certificiranje detail

The BTC certificiranje detail page, with a header (name, status, brand, date, komisija), an "Uredi certificiranje" button, a link to Rezultati certificiranja, and a collapsible Kandidati list. For each candidate it shows Modul 5 – Usmeni ispit with the current user's Zaključeno / Nije zaključeno status and an "Otvori" button that only komisija members see.

The BTC certificiranje detail page is accessible to all users with access to the BTC certificiranje section. It is opened with “Detalji certificiranja” from the list.

Breadcrumb: BTC certificiranje > {certificiranje name}. A “Natrag” link returns to the list.

  • Certificiranje name with a status tag (Aktivni / Nadolazeći / Prošli)
  • Brand · date
  • “Komisija:” followed by a comma-separated list of the komisija members’ names
  • “UREDI CERTIFICIRANJE” button – opens Uredi BTC certificiranje. Only shown to Trainers, and only when the status is Aktivni or Nadolazeći.
Figma shows one date in the header (e.g. “14.04.2026”), but a certificiranje has a start and an end date. Recommended: show the range, e.g. “14.04.2026 – 15.04.2026”, or one date when both are the same.

Rezultati certificiranja

A full-width link card, “Rezultati certificiranja →”, that opens the results page.

Kandidati

This section lists all candidates added to the certificiranje. Each candidate is a collapsible row that shows:

  • An avatar with the candidate’s initials
  • The candidate’s name
  • The candidate’s email · partner

An expanded row shows one activity:

  • Modul 5 – Usmeni ispit, with:
    • the form status for the current user: Zaključeno (green, with a check icon) or Nije zaključeno (grey, with a clock icon). When a form counts as Zaključeno is defined on the Modul 5 – Usmeni ispit page.
    • an “Otvori” button that opens the oral exam grading form. The button is only shown to komisija members of this certificiranje.

Rules:

  • Modul 5 – Usmeni ispit is added automatically to every candidate when the certificiranje is created.
  • Komisija members can still edit their form after it is Zaključeno.
Figma labels the activity “Modul 5 - Usmeni izpit” and the status “Završeno”. The Croatian spelling is “ispit”, and the status should be “Zaključeno”, as in the rest of the app.
Not specified: are candidate rows collapsed or expanded by default? Figma shows the first candidate expanded.

7.10 Editing existing BTC certificiranje

The "Uredi BTC certificiranje" form (Aktivni and Nadolazeći only). Same fields as Adding new BTC certificiranje, prefilled, with the "Uredi certificiranje" CTA. The end date can't be moved into the past. Candidates who already have grades cannot be removed. Removed komisija members keep their grades. Saving returns the user to the BTC certificiranje list with a success toast.

The user edits a certificiranje either from the actions menu on the BTC certificiranje list (“Uredi certificiranje”) or with the “UREDI CERTIFICIRANJE” button on the BTC certificiranje detail page.

Only certificiranja with status Aktivni or Nadolazeći can be edited.

There is no separate Figma screen for this page. It uses the Adding new BTC certificiranje screen with these differences:

  • Page title is “Uredi BTC certificiranje”
  • The CTA button is “Uredi certificiranje”
  • All fields are prefilled with the current data
Recommended CTA: “SPREMI CERTIFICIRANJE”, for consistency with “SPREMI ASSESSMENT” and the BTC školovanje edit screen in Figma.

Rules

  • Datum završetka cannot be moved into the past (before today). An edit therefore can’t make the certificiranje Prošli.
  • A candidate who already has any grades cannot be removed. The “–” button is not available for that candidate.
  • A komisija member can be removed while the certificiranje is in progress. Grades already entered by that member stay in the app.
  • Adding a candidate creates their Modul 5 – Usmeni ispit activity.

Saving

When the certificiranje is saved, the user is taken to the BTC certificiranje list and a success toast notification is shown.

7.11 Form – Modul 5 – Usmeni ispit

An individual oral exam grading form, filled in per candidate by each komisija member. Section a) has one Ocjena prezentacije grade (30 %). Section b) has 7 Igranje uloga category grades, whose average (70 %) is calculated automatically. Završna ocjena = (a × 3 + b × 7) / 10. The scale is 0–5 with one decimal. The form is Zaključeno when grade a and all 7 b grades are filled in.

Modul 5 – Usmeni ispit is an individual grading form. Each komisija member fills it in separately for each candidate. It is opened with “Otvori” next to the candidate on the BTC certificiranje detail page. Only komisija members of the certificiranje can open it.

Breadcrumb: BTC certificiranje > {certificiranje name}. A “Natrag” link returns to the certificiranje detail page.

Title: “Modul 5 – Usmeni ispit”. Subtitle: the certificiranje name.

Figma has “Usmeni izpit” in the title and “Assessment 2026/3” as the subtitle. The correct values are “Usmeni ispit” and the certificiranje name (e.g. “Certificiranje 2026/3”).
Unlike the individual assessment forms, the Figma sticky footer does not show the current candidate. Recommended: show the candidate’s avatar and name in the footer, as on the assessment forms.

The form is divided into two sections. The grade column header is “Ocjena (0 do 5)”.

a) Prezentacija poslovnog slučaja

Descriptive criteria. These are guidelines for the komisija member, without inputs:

  • Analitičko razmišljanje
  • Koncepcijsko razmišljanje
  • Komunikacija
  • Preuzimanje odgovornosti
  • Identificiranje s markom

One grade is entered for the whole section:

  • a) Ocjena prezentacije (teži 30%) – input. This is grade a.

b) Igranje uloga

7 categories. Each has one grade input, and its sub-criteria are listed below it as descriptive bullet points (guidelines only, without inputs).

1. Komunikacija

  • Kandidat postavlja korisna pitanja
  • Sluša aktivno
  • Ima primjeren govor tijela (očni kontakt i kretnje)
  • Vodi razgovor na primjeren način
  • Traži rješenja u konfliktnoj situaciji

2. Empatija / Tolerancija

  • Kandidat shvaća kupčevo stajalište
  • Reagira suosjećajno na raspoloženje kupca
  • Uvažava i prihvaća kupca
  • Zadobiva povjerenje

3. Osjećaj odgovornosti

  • Kandidat pokazuje odgovornost za sebe i svoje postupke
  • Pokazuje odgovornost prema auto kući
  • Pokazuje odgovornost prema imidžu marke
  • Pokazuje odgovornost prema kupcu

4. Samopouzdanje

  • Kandidat ima pozitivan stav
  • Ponaša se stabilno pod pritiskom
  • Djeluje samopouzdano ali ne i arogantno
  • Svjestan je jakosti kupca ili potencijalnog kupca i prihvaća ih

5. Izgled

  • Kandidat je obučen uredno i prikladno
  • Kandidat odgovara specifičnostima zemlje i svoje funkcije

6. Poznavanje proizvoda

  • Pokazuje znanja o svim relevantnim proizvodima marke
  • Ima odgovarajuće tehničko znanje
  • Svjestan je smjernica marke
  • Pokazuje znanje o matičnoj kući

7. Prodajni procesi

  • Kandidat učinkovito vodi prodajni razgovor

Calculated rows

  • b) Prosječna ocjena igranja uloga (teži 70%) — prosjek ocjena 1–7 – read-only. Calculated automatically as the average of the 7 category grades. This is grade b.
  • Završna ocjena usmenog ispita – read-only, calculated automatically with the formula (a × 3 + b × 7) / 10. It is used on the Rezultati certificiranja page.

Empty calculated values are shown as “—”.

Not specified: should grade b and the final grade be calculated as soon as some of the inputs are filled in (averaging only the filled categories), or only once all inputs are filled in? Recommended: calculate from the filled values, as the assessment forms do.

Input validation

  • Grades are on a scale of 0–5, and one decimal is allowed (e.g. 3,2).
  • No other numbers or characters can be entered.
  • Values are shown with a decimal comma.

Grading scale

Shown at the bottom of the form:

  • 5 = Natprosječno
  • 4 = Standard dosegnut i dokazan vrlo uvjerljivo
  • 3 = Standard dosegnut
  • 2 = Standard djelomice dosegnut i dokazan donekle uvjerljivo
  • 1 = Nije uvjerljivo dokazano
  • 0 = Nije nikako dokazano

Status of the form

The form is Zaključeno for the current komisija member and candidate when grade a and all 7 category grades in section b are filled in. Otherwise it is Nije zaključeno.

Additional

  • Each komisija member fills in their own grades independently and sees only their own input.
  • The form can still be edited after it is Zaključeno.
  • Saving:
    • “SPREMI” – saves and stays on the form
    • “SPREMI I ZATVORI” – saves and returns to the certificiranje detail page
    • A success toast notification is shown every time the form is saved.

7.12 Rezultati certificiranja

The Rezultati certificiranja page. A results table (activities × candidates) with Modul 5 – Pismeni ispit (a percentage a Trainer or BTC evaluator enters inline), Modul 5 – Usmeni ispit (the average of the komisija's final grades) and a Prošao / Nije prošao row (needs ≥ 75 % and ≥ 3). It also has CSV export and a "Pregled unosa članova komisije" table with each komisija member's status.

The results page shows a summary of results for all candidates. It is opened from the “Rezultati certificiranja” link card on the BTC certificiranje detail page.

Breadcrumb: BTC certificiranje > {certificiranje name}. A “Natrag” link returns to the certificiranje detail page.

Header:

  • Title “Rezultati certificiranja”
  • Subtitle: brand · date · number of candidates · certificiranje name
  • “IZVEZI U CSV” button – exports the results to CSV
Figma’s subtitle does not show the number of candidates, but the spec requires it.

Results table

Activities are rows and candidates are columns. The first column header is “Aktivnost / Kandidat”.

RowSource
Modul 5 – Pismeni ispitThe written exam result as a percentage, entered manually by a Trainer or BTC evaluator
Modul 5 – Usmeni ispitThe oral exam result: the average across all komisija members who have submitted grades, using each member’s Završna ocjena usmenog ispita. Shown with one decimal.
Prošao / Nije prošao (grey row, no label)“Prošao” (green) or “Nije prošao” (grey), calculated from the two rows above

Pass rule

A candidate passes (“Prošao”) when both of these are true:

  • Pismeni ispit is at least 75 %
  • Usmeni ispit is at least 3

Otherwise the result is “Nije prošao”. The result is only written when both grades have been entered. Until then the cell shows “—”.

If no result has been submitted for a candidate yet, the cell shows “—”.

Not specified: is the pass check done on the unrounded Usmeni ispit average or on the rounded one-decimal value shown (e.g. 2,96 shows as 3,0)? Recommended: use the value shown, so that what the user sees matches the result.
Figma shows Usmeni ispit as whole numbers (2, 3, 4), but the spec requires one decimal.

Editing Pismeni ispit

Figma: https://www.figma.com/design/NWu7ssnTfTJHFNDy13CnUt/Aplikacija-Vento---UI?node-id=3911-9096

Both Trainers and BTC evaluators can edit Pismeni ispit.

  • The Modul 5 – Pismeni ispit row has an edit (pencil) icon at the end of the row.
  • Clicking it turns the row’s cells into input fields, one per candidate, and the icon becomes a “SPREMI” button.
  • “SPREMI” saves the values and returns the row to read-only mode. The Prošao / Nije prošao row is recalculated.
  • An empty value is shown as “—”.
Not specified: input validation for Pismeni ispit. Recommended: 0–100, whole numbers or one decimal, shown with a “%” sign (Figma shows “90” without it).

Pregled unosa članova komisije

Below the results table, a second card shows submission status. Activities are rows and komisija members are columns. The first column header is “Aktivnost / Član komisije”.

Row:

  • Modul 5 – Usmeni ispit

Each cell shows one of:

  • Zaključeno – green, with a check icon
  • Nije zaključeno – grey, with a clock icon

The status is for that combination of activity and komisija member. If the member has not finished the form for every candidate, the cell shows Nije zaključeno.

Figma uses “Završeno” here. These docs use “Zaključeno”, as in the rest of the app.

7.13 Backend: Adding new BTC školovanje

Backend task for creating a BTC školovanje (POST /btc-trainings) – check the API spec and DB structure, set up the btc-trainings permissions, tables and lookups, create the 4 fixed modules, implement, and verify against the spec.

Docs: Adding new BTC školovanje Spec: POST /btc-trainings (createBtcTraining), BtcTrainingNewRequest, BtcTraining, BtcStatus in api-docs/openapi.yaml DB: BtcTrainings, BtcTrainingModules, BtcTrainingCandidates in api/database/create_schema.sql

This is the first BTC školovanje endpoint, so it also includes the shared setup for the školovanje sub-area: permissions, tables, the status helper and the Partner lookup.

1. Double check API spec and DB structure

  • Every docs field is in BtcTrainingNewRequest, with the right required/optional setting: Marka (multiselect → brandCodes), Naziv školovanja (name), Termini po modulima (modules, startDate / endDate optional), and candidates: Ime i prezime (required), E-mail (optional), Partner (required, partnerId), Marka (kandidat) (required, brandCode).

  • Limits: 1–20 candidates (minItems / maxItems), modules exactly 4 with moduleNumber 1–4, each once. Check the docs and the spec agree.

  • Date validation: the spec and the CK_BtcTrainingModules_Dates constraint check end ≥ start inside one module.

    Not specified: must the modules be in date order (each module starts after the previous module’s end date), or can they overlap or be in any order? If the order is required, the backend enforces it (→ 400 validation_error; also decide what happens when a module in between has no dates) and the rule goes into the BtcTrainingNewRequest / createBtcTraining description. See the comment on Adding new BTC školovanje.
  • Marka (kandidat): the docs ask whether its options are limited to the brands selected under Osnovni podaci. If yes, the backend must validate brandCode ∈ brandCodes (→ 400). Decide and update the spec.

  • E-mail format: the request has no format: email. Decide whether an invalid e-mail is rejected (→ 400) or stored as free text.

  • Default structure: the docs say 4 modules (each with Dan 1 / Dan 2), a Rezultati feedback forme page and a Potpisi page are created. In the schema this is only the 4 BtcTrainingModules rows. Attendance, feedback values and files start empty. The “feedback form” of each module is not part of the app. Confirm nothing else must be created.

  • Each DB column has a request field or a value set by the server (CreatedBy, CreatedAt). Use the same CreatedBy value (user id or display name) as Assessments.

  • Is 201 + BtcTraining enough for the next step (back to the list with a toast)?

  • Roles and permissions: the spec uses btc-trainings.edit here and btc-trainings.access on the read endpoints. Neither exists in api/Authorization/Permissions.cs. Per the BTC roles comment, only the Trainer role has access to BTC školovanje (Assessment evaluators and BTC evaluators don’t). Confirm, and confirm whether Admin also gets them.

  • Brand restriction: PermissionService.GetLimitedBrandCodes restricts some users to brands. Decide whether this applies to BTC školovanje (the docs say Trainers see everything, so recommended: no).

  • DB: BrandCodes VARCHAR(6) vs ^[SACVUL]{1,6}$; BtcTrainingCandidates.BrandCode CHAR(1) with the brand CHECK; UQ_BtcTrainingModules (TrainingId, ModuleNumber) with ModuleNumber 1–4 (at most 4 modules, each once); UQ_BtcTrainingCandidates_Slot caps candidates at 20 (the minimum is checked in code; Slot is not the display order, that’s CreatedAt).

  • Partner lookup: Partner is a dropdown. Today partnerId is free text and there is no partner source in api/. Use the same decision as Backend: Adding new assessment and reuse its endpoint.

2. Implementation

  • Roles and permissions: add btc-trainings.access and btc-trainings.edit to Permissions.cs and grant both to Trainer (and Admin, if confirmed). The Trainer role is added to RoleCode.cs by the assessments task. If that hasn’t been done yet, add it here (code, GetRoles catalog). Add the two names to the PermissionName enum check.
  • DB: apply the -- BTC ŠKOLOVANJE -- section of create_schema.sql (plus a migration script for existing environments, if used). Add the status helper from BtcStatus: today’s date in Europe/Zagreb (TimeZoneInfo “Europe/Zagreb”, as in EventInviteesRepository.Documents.cs); past when Modul 4 EndDate < today; otherwise upcoming when Modul 1 StartDate is null or > today; otherwise active. Write it so it can also be used as a SQL expression (the list filters and sorts by status).
  • Lookups: Partner dropdown from the shared partner source.
  • Function + repository. Insert BtcTrainings, the 4 BtcTrainingModules rows (dates from the request) and the candidates in one transaction (several writes). Candidates get Slot 1…n in request order.
  • Validation → 400 (validation_error): required fields; brandCodes pattern with no duplicate letters; 1–20 candidates; exactly modules 1–4; end ≥ start per module; lengths (name, fullName, email 255, partnerId 50); plus the rules decided in step 1.
  • Requires btc-trainings.edit → 403. Response 201 with BtcTraining (calculated status, module1StartDate, module4EndDate, candidatesCount).

3. Verify implementation against spec

  • Route, method, request and response shapes match createBtcTraining.
  • 400 / 401 / 403 / 500 cases can be produced; no undocumented status codes.
  • Assessment evaluator / BTC evaluator token → 403. 21 candidates → 400. Three modules, or module 2 twice → 400. Modul 1 end before start → 400.
  • Status of the created školovanje: no dates → upcoming; Modul 1 start today and no Modul 4 end → active; Modul 4 end yesterday → past.
  • GET /me/permissions for a Trainer includes btc-trainings.access and btc-trainings.edit. Permission names in Permissions.cs match the PermissionName enum.

Done when

A trainer can create a BTC školovanje, which then shows on the list and opens on the detail page with 4 empty modules.

7.14 Backend: BTC školovanje list

Backend task for the BTC školovanje list (GET /btc-trainings) – check search, date range, brand and status filters, status sorting and pagination against the API spec and DB structure, implement, and verify against the spec.

Docs: BTC školovanje list Spec: GET /btc-trainings (getBtcTrainings), BtcTraining, BtcStatus, Page / PerPage in api-docs/openapi.yaml DB: BtcTrainings, BtcTrainingModules, BtcTrainingCandidates in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Each list column maps to a field: Naziv → name, Marka → brandCodes, Datum početka Modul 1 → module1StartDate, Status → status.

  • Status without dates: the docs ask what status a školovanje without a Modul 1 start date has. The spec says upcoming, sorted last in its group (the docs recommendation). Confirm.

  • Date range filter (decided): any overlap with Modul 1 start → Modul 4 end, partial overlap included (open-ended without a Modul 4 end; never matches without a Modul 1 start). Check the spec and docs say the same.

  • Pagination: 10 per page, the same as the events list. The endpoint uses the shared Page / PerPage (default 20), so the frontend sends perPage=10.

    Please confirm 10 records per page. Neither the spec nor the events docs give a page size; 10 is what the events list uses in the app.
  • Search matches the školovanje name and candidate names (search). The brand filter matches the školovanje’s BrandCodes, not the candidates’ Marka (kandidat). Confirm.

  • Sorting: status (active → upcoming → past), then module1StartDate ascending, null last.

  • Actions menu by status (Detalji / Uredi / Ukloni) is decided by the frontend from status. Check that status is all it needs.

  • Roles and permissions: btc-trainings.access, Trainer only. No per-member filtering (unlike assessments). Brand restriction as decided in Backend: Adding new BTC školovanje.

  • DB: status is not stored. It needs Modul 1 StartDate and Modul 4 EndDate from BtcTrainingModules. Deleted rows (DeletedAt not null) are excluded.

2. Implementation

  • Roles and permissions: require btc-trainings.access → 403.
  • DB: one query that joins Modul 1 and Modul 4 per školovanje and calculates the status in SQL (today’s Zagreb date passed as a parameter), so filtering by status, sorting and paging all happen in SQL. Exclude DeletedAt IS NOT NULL.
  • Search: case-insensitive LIKE on Name and EXISTS on BtcTrainingCandidates FullName.
  • brandCode: BrandCodes LIKE '%X%'. dateRange: parse start/end the same way as getAssessments. Invalid values → 400.
  • candidatesCount in the same query (no N+1).
  • Response: pagination + results of BtcTraining.

3. Verify implementation against spec

  • Route, query parameters and response shape match getBtcTrainings.
  • Sorting with a mix of statuses and a školovanje without dates (last among upcoming).
  • Each status filter, brand filter, a search that matches only a candidate name, and a date range that overlaps only Modul 3.
  • totalResults is correct with filters applied; page 2 returns the rest.
  • A deleted školovanje doesn’t appear. Evaluator token → 403.

Done when

The BTC školovanje list page can be built from the API, with all filters, sorting and pages working.

7.15 Backend: BTC školovanje detail

Backend task for the BTC školovanje detail page and attendance (GET /btc-trainings/{trainingId}, POST /modules/{moduleNumber}/attendance) – check the response and save semantics against the API spec and DB structure, permissions, implement, and verify against the spec.

Docs: BTC školovanje detail Spec: GET /btc-trainings/{trainingId} (getBtcTrainingDetail), POST /btc-trainings/{trainingId}/modules/{moduleNumber}/attendance (setBtcTrainingAttendance), BtcTrainingDetail, BtcTrainingModule, BtcTrainingAttendanceRequest in api-docs/openapi.yaml DB: BtcTrainings, BtcTrainingModules, BtcTrainingCandidates, BtcTrainingAttendance in api/database/create_schema.sql

The detail endpoint is also used by the edit form and the Rezultati feedback forme page.

1. Double check API spec and DB structure

  • Header: name, status, brand(s) → name, status, brandCodes. “UREDI ŠKOLOVANJE” is shown only for active / upcoming. The frontend decides this from status.
  • Modules: always 4, ordered by moduleNumber, with startDate / endDate (null → “-”).
  • Attendance table: one entry per candidate per module with day1 / day2. The candidate name and initials come from candidates. Check this covers the table.
  • Save semantics: “Spremi” saves one day of one module. The spec replaces that day: the listed presentCandidateIds are present and every other candidate is not. Check this matches the docs (only one day in edit mode at a time).
  • Status rule: the docs don’t say whether attendance can be edited on a Prošli školovanje. The spec allows it in any status. Confirm.
  • Feedback form: the docs comment asks whether anything about the module feedback form is needed on the detail page. Nothing in the spec. Confirm.
  • hasData per candidate is used by the edit form (hides “–”). Check it’s true exactly when the candidate has any attendance row.
  • Roles and permissions: GET → btc-trainings.access; attendance → btc-trainings.edit. Both Trainer only.
  • DB: UQ_BtcTrainingAttendance (ModuleId, CandidateId, Day), Day IN (1, 2). No row = not present.

2. Implementation

  • Roles and permissions: GET requires btc-trainings.access; POST requires btc-trainings.edit → 403.
  • DB: GET loads the školovanje (not deleted, else 404), its candidates, its 4 modules and all attendance rows in a few queries (no query per module or candidate).
  • POST: 404 for an unknown / deleted školovanje; moduleNumber outside 1–4 → 400; day not 1 or 2 → 400; any id in presentCandidateIds not a candidate of this školovanje → 404. One MERGE, no transaction: target = the day’s rows for this module, source = the present candidates; insert the missing ones, delete the rest. Unique violation (the day saved twice at once) → retry once; FK error 547 (a candidate removed in the meantime) → 404.
  • POST response: the BtcTrainingModule with all candidates’ day1 / day2.

3. Verify implementation against spec

  • Shapes and status codes match getBtcTrainingDetail and setBtcTrainingAttendance.
  • Saving Dan 1 doesn’t change Dan 2 or other modules.
  • An empty presentCandidateIds clears the day.
  • A candidate from another školovanje → 404. Evaluator → 403. Deleted školovanje → 404.
  • After saving attendance, hasData of that candidate is true.

Done when

The detail page shows the 4 modules with attendance, and a trainer can save attendance for each day.

7.16 Backend: Editing existing BTC školovanje

Backend task for updating a BTC školovanje (POST /btc-trainings/{trainingId}) – check the status rule, candidate matching and the "candidate has data" rule against the API spec and DB structure, implement, and verify against the spec.

Docs: Editing existing BTC školovanje Spec: POST /btc-trainings/{trainingId} (updateBtcTraining), BtcTrainingNewRequest, BtcTrainingConflict in api-docs/openapi.yaml DB: BtcTrainings, BtcTrainingModules, BtcTrainingCandidates, BtcTrainingAttendance in api/database/create_schema.sql

The form is prefilled from GET /btc-trainings/{trainingId} (Backend: BTC školovanje detail).

1. Double check API spec and DB structure

  • Same fields and validation as create. Reuse the validation from Backend: Adding new BTC školovanje.
  • Prefill: the detail response has everything the form needs (module dates, candidates with partnerId, brandCode, hasData). Check.
  • Rule: only active / upcoming can be edited → 409 training_past.
  • Changing dates into the past (decided: not allowed): a Modul 4 end date before today (Europe/Zagreb) → 400 validation_error. Check the spec description of updateBtcTraining says so. Other module dates may be in the past (earlier modules have already happened).
  • Candidate with data: the docs say “any data (e.g. attendance)”. Attendance is the only candidate data in the schema. Confirm, then → 409 candidate_has_data.
  • Candidates are matched by id. An id that isn’t a candidate of this školovanje → 404. A new candidate is not present on any day (no attendance rows needed).
  • Modules are never added or removed; only their dates change. Attendance, Trener / Ocjena and signature files are kept.
  • CTA label (“Uredi školovanje” vs “SPREMI ŠKOLOVANJE”) is frontend only.
  • Response 200 + BtcTraining is enough to go back to the list with a toast.
  • Roles and permissions: btc-trainings.edit, Trainer only.
  • DB: UpdatedAt set on BtcTrainings, the changed modules and the changed candidates.
  • DB: “candidate has data” is enforced by the FK from BtcTrainingAttendance: deleting such a candidate fails with 547. UQ_BtcTrainingCandidates_Slot caps candidates at 20.

2. Implementation

  • Roles and permissions: require btc-trainings.edit → 403.
  • DB: in one transaction (several writes): check status (past → 409); delete removed candidates with one DELETE … WHERE TrainingId = @id AND Id NOT IN (…) (no check first: FK error 547 → roll back, 409 candidate_has_data); update BtcTrainings; update the 4 module rows by ModuleNumber; update existing candidates; insert new ones with the lowest free Slot. Commit only when every statement succeeds. Unique violation on Slot (another edit at the same time) → roll back and run the whole edit again once, in a new transaction; same failure again → 500.
  • Unknown / deleted školovanje → 404. Validation → 400, including a Modul 4 end date before today. Conflicts → 409 with the error code. Nothing is saved when any check fails.
  • Response 200 with the recalculated BtcTraining.

3. Verify implementation against spec

  • Route, request and response match updateBtcTraining; 409 codes match BtcTrainingConflict.
  • Modul 4 end date yesterday → 400; today → saved and still active. Modul 1 end date in the past → saved.
  • Past školovanje → 409 training_past. Removing a candidate with attendance → 409 candidate_has_data. Removing one without attendance → removed.
  • Adding a candidate → appears in every module’s attendance with day1 / day2 false.
  • Attendance and signature files are unchanged after an edit. Evaluator → 403.

Done when

A trainer can edit an active or upcoming školovanje, without losing attendance or files.

7.17 Backend: Potpisi page

Backend task for the Potpisi signature files (GET/POST/DELETE /btc-trainings/{trainingId}/…/signature-files, download) – check file types, size limits and delete behaviour against the API spec and DB structure, add blob storage handling, implement, and verify against the spec.

Docs: Potpisi page Spec: GET /btc-trainings/{trainingId}/signature-files (getBtcTrainingSignatureFiles), POST /btc-trainings/{trainingId}/modules/{moduleNumber}/signature-files (uploadBtcTrainingSignatureFiles), DELETE /btc-trainings/{trainingId}/signature-files/{fileId} (deleteBtcTrainingSignatureFile), GET /btc-trainings/{trainingId}/signature-files/{fileId}/download (downloadBtcTrainingSignatureFile), BtcTrainingSignatureFile, BtcTrainingSignatureModule, BtcSignatureFileInvalid in api-docs/openapi.yaml DB: BtcTrainingSignatureFiles, BtcTrainingModules in api/database/create_schema.sql

1. Double check API spec and DB structure

  • The page lists 4 module cards with their files. getBtcTrainingSignatureFiles returns all 4 modules, also those without files. The subtitle (školovanje name) comes from GET /btc-trainings/{trainingId}. Confirm the frontend makes both calls, or add the name to this response.

  • File types: the docs say “jpg, png, pdf …”. The spec allows only JPG, PNG and PDF (file_type_not_allowed). Confirm the list (e.g. HEIC from phones?).

  • Limits: the docs leave the max file size and file count open. The spec sets 10 MB per file and no count limit. Confirm, and check the Functions host request size limit covers several files in one upload.

  • “Uredi” button and delete confirmation: the docs comment recommends “Uredi” only switches the card to edit mode (frontend only) and “Odaberi datoteke” uploads immediately. Confirm; nothing in the API depends on it.

  • Delete (decided): a file the user removes with x is deleted permanently (row + blob). The 2-year retention rule (see BTC) doesn’t apply to it.

  • When a file can’t be deleted: the docs don’t say, and the spec allows deleting in any status, including Prošli.

    Not specified: when can a signature file no longer be deleted? Options: never blocked (current spec); blocked once the školovanje is Prošli; or blocked after a time limit from the upload. If it’s blocked, add a 409 error code (e.g. training_past) to deleteBtcTrainingSignatureFile, and hide the x on the Potpisi page.
  • Status rule: the spec allows uploading and deleting in any status (including Prošli, since lists are often scanned after the training). Confirm.

  • After the školovanje is deleted (soft), all four endpoints → 404, but the files stay in blob storage. Confirm.

  • Roles and permissions: list + download → btc-trainings.access; upload + delete → btc-trainings.edit. Trainer only.

  • DB: FileName (original, shown) and BlobName (in container btc-signatures) are separate. The content type is not stored in the row; it is set on the blob at upload.

2. Implementation

  • Roles and permissions: check the permission per endpoint → 403.
  • Blob storage: add btc-signatures as a container constant in BlobStorageService and add a DeleteAsync method (it has only Download / Exists / Upload today). Blob name e.g. {trainingId}/{moduleNumber}/{fileId}{ext}, never the user’s file name.
  • Multipart: functions use HttpRequestData today. Read the multipart body with the ASP.NET Core integration (ConfigureFunctionsWebApplication is enabled, so HttpRequest / ReadFormAsync can be used) or a MultipartReader.
  • Upload: validate every file first (none → 400 validation_error; type by extension and content signature → 400 file_type_not_allowed; > 10 MB → 400 file_too_large). Then upload the blobs (with the validated content type as the blob’s ContentType) and insert the rows in one transaction. If the insert fails, delete the blobs already uploaded. Response 201 with the new files.
  • Store a sanitised original file name (max 255 characters, no path parts).
  • Delete: the file must belong to a module of this školovanje (else 404). Delete the row, then the blob; a missing blob doesn’t fail the request. Response 204.
  • Download: stream the blob with the content type from the blob’s properties and Content-Disposition: attachment with the original name (RFC 5987 filename* so č, š, ž survive).

3. Verify implementation against spec

  • Routes, multipart field name files, response shapes and status codes match the four operations; every error in BtcSignatureFileInvalid can be produced.
  • Upload 2 files to Modul 2 → both listed under module 2 only, in upload order.
  • One valid file + one .exe renamed to .pdf → 400, nothing stored.
  • 11 MB file → 400 file_too_large. Downloading returns the original name with Croatian characters.
  • A file id from another školovanje → 404. Evaluator → 403.

Done when

A trainer can upload, download and delete signature lists per module, and the files are kept safely in blob storage.

7.18 Backend: Rezultati feedback forme

Backend task for the Rezultati feedback forme values (POST /btc-trainings/{trainingId}/modules/{moduleNumber}/feedback) – check the Trener and Ocjena formats against the API spec and DB structure, implement, and verify against the spec.

Docs: Rezultati feedback forme Spec: POST /btc-trainings/{trainingId}/modules/{moduleNumber}/feedback (setBtcTrainingFeedback), GET /btc-trainings/{trainingId} (getBtcTrainingDetail), BtcTrainingFeedbackRequest, BtcFeedbackGrade, BtcTrainingModule in api-docs/openapi.yaml DB: BtcTrainingModules (Trainers, FeedbackGrade) in api/database/create_schema.sql

The page reads its data (školovanje name, 4 modules with trainers / feedbackGrade) from the detail endpoint. This task adds only the save endpoint.

1. Double check API spec and DB structure

  • Each table column maps to a field: Modul → moduleNumber, Trener → trainers, Ocjena → feedbackGrade. null renders as “—”.
  • Ocjena range: the docs comment says it isn’t specified; Figma shows two decimals (4,23). The spec and DB use 1–5 with two decimals (multipleOf: 0.01, DECIMAL(3,2), CHECK BETWEEN 1 AND 5). Confirm the scale of the feedback form.
  • Trener: free text, several trainers in one field, max 500 characters (NVARCHAR(500)). Confirm the length.
  • Save semantics: both values are replaced; null clears one. The frontend must always send both. Check against the docs (one row edited at a time, “SPREMI”).
  • Status rule: the spec allows saving in any status (feedback is often entered after the training). Confirm.
  • Roles and permissions: btc-trainings.edit, Trainer only.

2. Implementation

  • Roles and permissions: require btc-trainings.edit → 403.
  • DB: update Trainers, FeedbackGrade and UpdatedAt of the module row found by TrainingId + ModuleNumber.
  • Validation → 400: moduleNumber 1–4; feedbackGrade null or 1–5 with at most two decimals; trainers ≤ 500 characters (trim, empty string → null).
  • Unknown / deleted školovanje → 404. Response 200 with the BtcTrainingModule.

3. Verify implementation against spec

  • Route, request and response match setBtcTrainingFeedback.
  • Save Modul 3 → only Modul 3 changes in getBtcTrainingDetail.
  • feedbackGrade 4.235, 0.5 or 6 → 400. Both null → “—” again.
  • Evaluator → 403. Deleted školovanje → 404.

Done when

A trainer can enter and change Trener and Ocjena for each module, and the page shows them.

7.19 Backend: Delete BTC školovanje

Backend task for deleting a BTC školovanje (DELETE /btc-trainings/{trainingId}) – soft delete, check permissions and the upcoming-only rule, implement, and verify against the spec.

Docs: BTC školovanje list (Actions menu → Ukloni školovanje, Delete confirmation modal) Spec: DELETE /btc-trainings/{trainingId} (deleteBtcTraining), BtcTrainingConflict in api-docs/openapi.yaml DB: BtcTrainings (DeletedAt) in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Soft delete (decided): the spec soft-deletes (sets DeletedAt) and keeps candidates, attendance, feedback values and signature files. Data is kept for 2 years (see BTC). Assessments and BTC certificiranje use the same approach.
  • Rule: only upcoming can be deleted → 409 training_not_upcoming. Check the docs agree (Aktivni and Prošli can’t be deleted).
  • After deleting, every BTC školovanje endpoint (detail, edit, attendance, feedback, signature files, download) → 404, and the list excludes it.
  • Roles and permissions: btc-trainings.edit, Trainer only.

2. Implementation

  • Roles and permissions: require btc-trainings.edit → 403.
  • DB: set DeletedAt (and UpdatedAt). Make sure the shared “load školovanje” helper used by the other endpoints filters DeletedAt IS NULL.
  • Status check with the shared status helper → 409. Unknown or already deleted id → 404. Success → 204.

3. Verify implementation against spec

  • Status codes and the error value match deleteBtcTraining.
  • Active / past školovanje → 409. Evaluator → 403. Deleting twice → 404 the second time.
  • After deleting: list excludes it, detail and signature download → 404.

Done when

A trainer can delete an upcoming školovanje, and it disappears everywhere in the app while its data stays in the database.

7.20 Backend: Adding new BTC certificiranje

Backend task for creating a BTC certificiranje (POST /btc-certifications) – check the API spec and DB structure, set up the BTC evaluator role, btc-certifications permissions, tables and the Komisija / Partner lookups, implement, and verify against the spec.

Docs: Adding new BTC certificiranje Spec: POST /btc-certifications (createBtcCertification), BtcCertificationNewRequest, BtcCertification, BtcStatus in api-docs/openapi.yaml DB: BtcCertifications, BtcCertificationCandidates, BtcCertificationBoardMembers in api/database/create_schema.sql

This is the first BTC certificiranje endpoint, so it also includes the shared setup for the certificiranje sub-area: the BTC evaluator role, permissions, tables, the status helper and the Komisija / Partner lookups.

1. Double check API spec and DB structure

  • Every docs field is in BtcCertificationNewRequest, with the right required/optional setting: Marka (one brand → brandCode), Naziv certificiranja (name), Datum početka / završetka (startDate / endDate, both required), candidates (Ime i prezime required, E-mail optional, Partner required), Komisija (boardMemberUserIds).
  • Limits: 1–20 candidates, 1–10 komisija members, no duplicate members. Check the docs and the spec (minItems / maxItems) agree.
  • Dates: end ≥ start (docs recommendation) is in the spec and in CK_BtcCertifications_Dates. Confirm.
  • Komisija lookup: each row is one user dropdown whose options show the user’s name and e-mail (there is no separate e-mail field). The docs recommend listing only Trainer and BTC evaluator users. The spec has no users endpoint and api/ has no users integration. Use the same source as Backend: Adding new assessment (it must return user id, full name, e-mail and role) and filter by role. The e-mail is only shown in the dropdown; it is not stored (only UserId and FullName).
  • Partner lookup: Partner is a dropdown. Reuse the shared partner source.
  • Default structure: Rezultati certificiranja and Modul 5 – Usmeni ispit per candidate are fixed in code. No rows are created besides the certificiranje, candidates and komisija.
  • Each DB column has a request field or a value set by the server (CreatedBy, CreatedAt, member FullName).
  • Is 201 + BtcCertification enough to return to the list with a toast?
  • Roles and permissions (decided, see Roles): a BTC evaluator sees everything and can enter Pismeni ispit, but can’t create, edit or delete certificiranja. Mapping: Trainer → btc-certifications.access, btc-certifications.edit, btc-certifications.written-exam-edit; BTC evaluator → btc-certifications.access, btc-certifications.written-exam-edit. Admin → to confirm. This endpoint uses btc-certifications.edit.
  • Brand restriction: decide whether limitedBrandCodes applies (the docs say Trainers and BTC evaluators see everything; recommended: no).
  • DB: BrandCode CHAR(1) with the brand CHECK; PartnerId NVARCHAR(50) NOT NULL; UserId NVARCHAR(100). The maximums are capped by Slot: UQ_BtcCertificationCandidates_Slot (1–20) and UX_BtcCertificationBoardMembers_Slot (1–10, active members only); UQ_BtcCertificationBoardMembers_User keeps one row per user. The minimums are checked in code only. Slot is not the display order (that’s CreatedAt).

2. Implementation

  • Roles and permissions: add BtcEvaluator to RoleCode (pick a single-letter code, add ToName and the GetRoles catalog). The Trainer role comes from the assessments task; add it here if it doesn’t exist yet. Add btc-certifications.access, btc-certifications.edit and btc-certifications.written-exam-edit to Permissions.cs and map them to roles as decided in step 1. Update docs/content/general/user-roles.md if the role list is maintained there.
  • DB: apply the -- BTC CERTIFICIRANJE -- section of create_schema.sql (plus a migration script, if used). Add the status helper from BtcStatus: upcoming when StartDate > today, past when EndDate < today, otherwise active (today in Europe/Zagreb). Make it usable as a SQL expression for the list.
  • Lookups: Komisija users (Trainer and BTC evaluator, with name and e-mail for the dropdown) and partners, from the shared sources. A service that resolves a user id to full name and role.
  • Function + repository. Insert the certificiranje, candidates and komisija members in one transaction (several writes). Candidates get Slot 1…n and members 1…m, in request order.
  • Validation → 400 (validation_error): required fields; brand code; end ≥ start; 1–20 candidates; 1–10 unique members; every member exists and has the Trainer or BTC evaluator role; lengths.
  • Requires btc-certifications.edit → 403. Response 201 with BtcCertification (calculated status, candidatesCount).

3. Verify implementation against spec

  • Route, method, request and response shapes match createBtcCertification.
  • 400 / 401 / 403 / 500 cases can be produced; no undocumented status codes.
  • BTC evaluator token → 403. End before start → 400. Duplicate member → 400. A user with the Assessment evaluator role as member → 400.
  • GET /me/permissions returns the expected set for a Trainer and for a BTC evaluator. Permission names in Permissions.cs match the PermissionName enum.
  • Any new lookup endpoints are in openapi.yaml.

Done when

A trainer can create a BTC certificiranje, which then shows on the list and on the detail page with its candidates and komisija.

7.21 Backend: BTC certificiranje list

Backend task for the BTC certificiranje list (GET /btc-certifications) – check search, date, brand and status filters, status sorting and 10-per-page pagination against the API spec and DB structure, implement, and verify against the spec.

Docs: BTC certificiranje list Spec: GET /btc-certifications (getBtcCertifications), BtcCertification, BtcStatus, Page / PerPage in api-docs/openapi.yaml DB: BtcCertifications, BtcCertificationCandidates in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Each list column maps to a field: Naziv → name, Marka → brandCode, Datum početka → startDate, Datum završetka → endDate, Status → status.
  • Date filter: Figma shows a single-date picker; the docs recommend a date range, as on Assessments and BTC školovanje. The spec takes dateRange and matches any overlap with start → end (a single day = same start and end). Confirm.
  • Pagination: 10 per page. The shared PerPage defaults to 20, so the frontend sends perPage=10. Confirm, or give this endpoint its own default.
  • Search matches the name and candidate names. Sorting: status (active → upcoming → past), then startDate ascending.
  • Actions menu by status is decided by the frontend from status. Edit / delete actions are only shown to users with btc-certifications.edit. The frontend reads this from GET /me/permissions. Confirm.
  • Roles and permissions: btc-certifications.access. Trainers and BTC evaluators see all certificiranja (no per-member filtering, unlike assessments). Check against the docs.
  • DB: status is not stored. Deleted rows (DeletedAt not null) are excluded. IX_BtcCertifications_StartDate supports the sort.

2. Implementation

  • Roles and permissions: require btc-certifications.access → 403.
  • DB: one query that calculates the status in SQL (today’s Zagreb date as a parameter), so filtering by status, sorting and paging all happen in SQL. Exclude DeletedAt IS NOT NULL.
  • Search: case-insensitive LIKE on Name and EXISTS on BtcCertificationCandidates FullName. brandCode: equality. dateRange: overlap, parsed the same way as getAssessments. Invalid values → 400.
  • candidatesCount in the same query.
  • Response: pagination + results of BtcCertification.

3. Verify implementation against spec

  • Route, query parameters and response shape match getBtcCertifications.
  • Sorting with a mix of statuses. Each status filter, brand filter, a search that matches only a candidate name, and a single-day dateRange inside a 2-day certificiranje.
  • perPage=10 with 14 records → 10 + 4, totalResults 14.
  • A BTC evaluator who is not in any komisija still sees all certificiranja. A user without btc-certifications.access → 403. A deleted certificiranje doesn’t appear.

Done when

The BTC certificiranje list page can be built from the API, with all filters, sorting and pages working.

7.22 Backend: BTC certificiranje detail

Backend task for the BTC certificiranje detail page (GET /btc-certifications/{certificationId}) – check the header, komisija, candidates and the caller's Usmeni ispit status against the API spec and DB structure, permissions, implement, and verify against the spec.

Docs: BTC certificiranje detail Spec: GET /btc-certifications/{certificationId} (getBtcCertificationDetail), BtcCertificationDetail in api-docs/openapi.yaml DB: BtcCertifications, BtcCertificationCandidates, BtcCertificationBoardMembers, BtcCertificationGrades in api/database/create_schema.sql

The detail endpoint is also used by the edit form and for the header / candidate name on the Usmeni ispit form.

1. Double check API spec and DB structure

  • Header: name, status, brand, date, “Komisija:” list → name, status, brandCode, startDate / endDate, boardMembers (active only). The docs recommend showing the date range; the response has both dates.
  • Candidates: name, e-mail, partner → fullName, email, partnerId. The partner name comes from the partner lookup. Check the frontend can resolve it (or add partnerName).
  • Usmeni ispit row: the caller’s status → myOralExamCompleted; “Otvori” only for komisija members → isBoardMember. Both use active membership: a removed member sees no “Otvori”. Confirm.
  • “UREDI CERTIFICIRANJE” is shown for Aktivni / Nadolazeći and only to users with btc-certifications.edit (Trainer; hidden from BTC evaluators, per the docs). The frontend decides this from status and GET /me/permissions.
  • hasGrades per candidate (any member’s grade, removed members included, or a written exam score) is used by the edit form to hide “–”.
  • Komisija members can keep editing after Zaključeno and in any certificiranje status. Nothing here blocks that.
  • Roles and permissions: btc-certifications.access (Trainer, BTC evaluator), all certificiranja.
  • DB: removed members (DeletedAt not null) are left out of boardMembers.

2. Implementation

  • Roles and permissions: require btc-certifications.access → 403.
  • DB: load the certificiranje (not deleted, else 404), active members, candidates, and the caller’s grades for all candidates (one query), plus a “has grades” flag per candidate (one query with EXISTS).
  • isBoardMember: the caller’s user id matches an active BtcCertificationBoardMembers row.
  • myOralExamCompleted: use the shared Zaključeno helper from Backend: Form – Modul 5 – Usmeni ispit (grade a + all 7 categories). null when isBoardMember is false.

3. Verify implementation against spec

  • Shape and status codes match getBtcCertificationDetail.
  • Komisija member: isBoardMember true, status per candidate correct after saving a full and a partial form.
  • Trainer not in komisija: isBoardMember false, myOralExamCompleted null.
  • Removed member: not in boardMembers, isBoardMember false, their grades still make hasGrades true.
  • Unknown / deleted id → 404. User without access → 403.

Done when

The detail page shows the header, komisija and candidates, with the caller’s own Usmeni ispit status and “Otvori” for komisija members only.

7.23 Backend: Editing existing BTC certificiranje

Backend task for updating a BTC certificiranje (POST /btc-certifications/{certificationId}) – check the status rule, candidate and komisija matching, soft-deleted members and the "candidate has grades" rule against the API spec and DB structure, implement, and verify against the spec.

Docs: Editing existing BTC certificiranje Spec: POST /btc-certifications/{certificationId} (updateBtcCertification), BtcCertificationNewRequest, BtcCertificationConflict in api-docs/openapi.yaml DB: BtcCertifications, BtcCertificationCandidates, BtcCertificationWrittenExamScores, BtcCertificationBoardMembers, BtcCertificationGrades in api/database/create_schema.sql

The form is prefilled from GET /btc-certifications/{certificationId} (Backend: BTC certificiranje detail).

1. Double check API spec and DB structure

  • Same fields and validation as create. Reuse the validation from Backend: Adding new BTC certificiranje.
  • Prefill: the detail response has the candidates (with partnerId, hasGrades) and the active komisija (userId, fullName). The dropdown label (name and e-mail) comes from the users lookup. Check a komisija member whose user was deleted can still be shown (fall back to fullName).
  • Rule: only active / upcoming can be edited → 409 certification_past.
  • Changing dates into the past (decided: not allowed): endDate before today (Europe/Zagreb) → 400 validation_error. Check the spec description of updateBtcCertification says so. startDate may be in the past (an active certificiranje).
  • Candidate with grades: “any grades” = an oral exam grade from any member, including removed members, or a written exam score → 409 candidate_has_grades. Confirm the written exam score counts.
  • Komisija: members left out are soft-deleted (DeletedAt); their grades stay and still count in the results (Backend: Rezultati certificiranja). Re-adding a removed member restores the same row (same Id, DeletedAt cleared), so their grades come back. Confirm, and decide whether FullName is refreshed on restore.
  • Candidates are matched by id. An id that isn’t a candidate of this certificiranje → 404. A new candidate gets Modul 5 – Usmeni ispit automatically (fixed in code, no rows).
  • CTA label (“Uredi certificiranje” vs “SPREMI CERTIFICIRANJE”) is frontend only.
  • Roles and permissions: btc-certifications.edit, Trainer only.
  • DB: UQ_BtcCertificationBoardMembers_User (CertificationId, UserId) keeps one row per user. It’s deliberately not filtered on DeletedAt: a re-added member must restore the soft-deleted row, and a filtered index would allow a second one.
  • DB: “candidate has grades” is enforced by the FKs from BtcCertificationGrades and BtcCertificationWrittenExamScores: deleting such a candidate fails with 547. The maximums are capped by Slot (soft-deleted members have Slot NULL).

2. Implementation

  • Roles and permissions: require btc-certifications.edit → 403.
  • DB: in one transaction (several writes), in this order:
    • check status (past → 409)
    • delete removed candidates with one DELETE … WHERE CertificationId = @id AND Id NOT IN (…); no check first: FK error 547 → roll back, 409 candidate_has_grades
    • update BtcCertifications; update existing candidates; insert new ones with the lowest free Slot
    • soft-delete removed members (DeletedAt, Slot NULL) first, then restore re-added ones and insert new ones (copy FullName from the users source), each with the lowest free Slot
    • commit only when every statement succeeds
    • unique violation (2601 / 2627) on Slot or UserId (another edit at the same time) → roll back and run the whole edit again once, in a new transaction; same failure again → 500
  • Validation → 400 (including endDate before today), conflicts → 409 with the error code, unknown / deleted id → 404. Nothing is saved when any check fails.
  • Response 200 with the recalculated BtcCertification.

3. Verify implementation against spec

  • Route, request and response match updateBtcCertification; 409 codes match BtcCertificationConflict.
  • endDate yesterday → 400; today → saved and still active.
  • Past certificiranje → 409 certification_past. Removing a candidate graded by a removed member → 409 candidate_has_grades.
  • Remove a member who graded, then re-add them → same row, grades visible again in their form.
  • Adding a candidate → listed on the detail page with Usmeni ispit Nije zaključeno. BTC evaluator → 403.

Done when

A trainer can edit an active or upcoming certificiranje, and no grades are lost when candidates or komisija members change.

7.24 Backend: Form – Modul 5 – Usmeni ispit

Backend task for the oral exam grading form (GET/POST /btc-certifications/{certificationId}/candidates/{candidateId}/oral-exam) – check the criterion keys, 0–5 scale, calculations and Zaključeno rule against the API spec and DB structure, komisija access, implement, and verify against the spec.

Docs: Form – Modul 5 – Usmeni ispit Spec: GET /btc-certifications/{certificationId}/candidates/{candidateId}/oral-exam (getBtcOralExamForm), POST …/oral-exam (saveBtcOralExamForm), BtcOralExamForm, BtcOralExamSaveRequest, BtcOralExamCriterion, BtcOralExamGrade in api-docs/openapi.yaml DB: BtcCertificationGrades, BtcCertificationBoardMembers, BtcCertificationCandidates in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Criterion keys: the docs inputs map to the 8 BtcOralExamCriterion values and the DB CHECK:

    • a) Prezentacija poslovnog slučaja – 1 input, Ocjena prezentacije → presentation (its 5 descriptive criteria have no inputs)
    • b) Igranje uloga – 7 inputs, one per category (the bullet points are guidelines only): Komunikacija (5 bullets) → communication, Empatija / Tolerancija (4) → empathy, Osjećaj odgovornosti (4) → responsibility, Samopouzdanje (4) → self-confidence, Izgled (2) → appearance, Poznavanje proizvoda (4) → product-knowledge, Prodajni procesi (1) → sales-process

    Agree the keys with the frontend.

  • Scale: 0–5 with one decimal: spec (minimum 0, maximum 5, multipleOf 0.1), DB (DECIMAL(2,1), CHECK BETWEEN 0 AND 5) and docs agree.

  • Calculations (spec, unrounded; the frontend rounds for display):

    • rolePlayGrade (grade b) = average of the entered category grades; null if none
    • finalGrade = (a × 3 + b × 7) / 10, only when grade a and grade b both have a value

    The docs comment leaves open whether b and the final grade are calculated from partial input. The spec follows the docs recommendation (from the filled values). Confirm.

  • Zaključeno: isCompleted when presentation and all 7 categories are entered.

  • Save semantics: the body is the full form state; criteria left out are cleared. Check this matches “Spremi” / “Spremi i zatvori” (both send the whole form).

  • Access: only active komisija members of the certificiranje (docs: “Only komisija members … can open it”). A Trainer not in the komisija and a removed member → 403. Allowed in any status, also after Zaključeno.

  • Response sufficiency: the form header needs the certificiranje name and the docs recommend showing the candidate in the footer. The response has only candidateId; the frontend gets both from the detail endpoint. Confirm, or add them here.

  • Roles and permissions: btc-certifications.access + active membership.

  • DB: UQ_BtcCertificationGrades (CandidateId, BoardMemberId, CriterionKey) keeps one row per criterion even when the form is saved twice at once.

2. Implementation

  • Roles and permissions: require btc-certifications.access → 403, then active membership of the caller → 403.
  • Shared helper: one function that takes a member’s grades for a candidate and returns rolePlayGrade, finalGrade and isCompleted. The detail and results endpoints use the same helper.
  • GET: unknown / deleted certificiranje, or a candidate of another certificiranje → 404. Return the caller’s grades and the calculated values.
  • POST: validation → 400 (unknown criterionKey, a key twice, grade outside 0–5 or more than one decimal). One MERGE, no transaction: target = the caller’s rows for this candidate, source = the body; update matches (UpdatedAt), insert new ones, delete the rest. Unique violation (saved twice at once) → retry once; FK error 547 (candidate removed in the meantime) → 404. Return the saved form.

3. Verify implementation against spec

  • Shapes and status codes match getBtcOralExamForm and saveBtcOralExamForm.
  • Hand-calculated example: a = 4, categories 3, 4, 5, 3, 4, 4, 5 → b = 4, final = 4.0, Zaključeno. Remove one category → b = average of 6, Nije zaključeno. Remove a → final null.
  • Grade 5.5, 3.25 or “x” → 400.
  • Two members grading the same candidate don’t see each other’s grades.
  • Trainer not in komisija → 403. Removed member → 403. Past certificiranje → save still works.

Done when

Each komisija member can fill in and change their own Usmeni ispit grades per candidate, with grade b, the final grade and Zaključeno calculated as in the docs.

7.25 Backend: Rezultati certificiranja

Backend task for the results page (GET /btc-certifications/{certificationId}/results, POST /written-exam-scores) – check the Usmeni ispit average, the pass rule and the komisija overview against the API spec and DB structure, permissions, implement, and verify with a hand-calculated example.

Docs: Rezultati certificiranja Spec: GET /btc-certifications/{certificationId}/results (getBtcCertificationResults), POST /btc-certifications/{certificationId}/written-exam-scores (setBtcCertificationWrittenExamScores), BtcCertificationResults, BtcWrittenExamScore in api-docs/openapi.yaml DB: BtcCertificationGrades, BtcCertificationCandidates, BtcCertificationWrittenExamScores, BtcCertificationBoardMembers in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Each results table row maps to a field: Modul 5 – Pismeni ispit → writtenExam, Modul 5 – Usmeni ispit → oralExam, Prošao / Nije prošao → passed. null renders as “—”.
  • Usmeni ispit average: the docs say “the average across all komisija members who have submitted their grades”. The spec averages the finalGrade of every member (including removed members) who has a final grade, even if the form isn’t Zaključeno. Decide whether only Zaključeno forms count, and whether removed members count.
  • Pass rule: passed is null until both values exist; true when writtenExam ≥ 75 and oralExam rounded to one decimal (half away from zero) ≥ 3. The docs comment leaves the rounding open; the spec follows the docs recommendation (use the value shown). Confirm.
  • Pismeni ispit format: the docs comment recommends 0–100 with whole numbers or one decimal, shown with “%”. Spec (multipleOf: 0.1) and DB (BtcCertificationWrittenExamScores.Score DECIMAL(4,1), CHECK 0–100) follow that. Confirm. One row per candidate that has a score (no row = no score); a separate table so its FK blocks removing a candidate who has a score.
  • Pregled unosa članova komisije: one column per active member with isCompleted = Zaključeno for every candidate. Confirm removed members are left out.
  • Header subtitle (brand · date · number of candidates · name) → certification in the response (candidatesCount included).
  • CSV export is done in the frontend from this response. Confirm it has everything the CSV needs.
  • Status rule: the spec allows entering Pismeni ispit in any status (the written exam results often arrive after the end date). Confirm.
  • Roles and permissions: GET → btc-certifications.access (Trainer and BTC evaluator, all certificiranja). POST → btc-certifications.written-exam-edit (Trainer and BTC evaluator; see Roles). The permission is added in Backend: Adding new BTC certificiranje.

2. Implementation

  • Roles and permissions: GET requires btc-certifications.access; POST requires btc-certifications.written-exam-edit → 403.
  • DB: one query for all grades of the certificiranje (all members, removed included), then calculate in code with the shared helper from Backend: Form – Modul 5 – Usmeni ispit: per member and candidate finalGrade → average per candidate → passed.
  • Komisija overview: active members only, isCompleted from the same helper for every candidate.
  • POST: update only the listed candidates (null clears the score); each candidateId must belong to the certificiranje (404); the same candidate twice → 400; score 0–100 with at most one decimal (400). Return the recalculated results. One MERGE on BtcCertificationWrittenExamScores, no transaction: a value inserts or updates the row, null deletes it. FK error 547 (candidate removed in the meantime) → 404; unique violation (saved twice at once) → retry once.
  • Unknown / deleted certificiranje → 404. Values are returned unrounded.

3. Verify implementation against spec

  • Shapes and status codes match both operations.
  • Results checked against a hand-calculated example: 3 members (one removed after grading), 3 candidates, one member with a partial form, written exam 74.9 / 75 / none. Include an oralExam of 2.96 (shown 3,0 → passes with ≥ 75 %) and 2.94 (fails).
  • A candidate with no grades → oralExam and passed are null.
  • BTC evaluator → GET 200, POST 200. A user without btc-certifications.written-exam-edit → POST 403. Score 100.5 or 75.25 → 400.

Done when

The results page and CSV can be built from the API, the trainer can enter Pismeni ispit, and the numbers and Prošao / Nije prošao match the hand-calculated example.

7.26 Backend: Delete BTC certificiranje

Backend task for deleting a BTC certificiranje (DELETE /btc-certifications/{certificationId}) – soft delete, check permissions and the upcoming-only rule, implement, and verify against the spec.

Docs: BTC certificiranje list (Actions menu → Ukloni certificiranje, Delete confirmation modal) Spec: DELETE /btc-certifications/{certificationId} (deleteBtcCertification), BtcCertificationConflict in api-docs/openapi.yaml DB: BtcCertifications (DeletedAt) in api/database/create_schema.sql

1. Double check API spec and DB structure

  • Soft delete (decided): the spec soft-deletes (sets DeletedAt) and keeps candidates, komisija and grades. Data is kept for 2 years (see BTC), the same as Backend: Delete BTC školovanje and Assessments.
  • Rule: only upcoming can be deleted → 409 certification_not_upcoming. Check the docs agree (Aktivni and Prošli can’t be deleted).
  • After deleting, every BTC certificiranje endpoint (detail, edit, oral exam form, results, written exam scores) → 404, and the list excludes it.
  • Roles and permissions: btc-certifications.edit, Trainer only (BTC evaluators can’t delete; see Roles).

2. Implementation

  • Roles and permissions: require btc-certifications.edit → 403.
  • DB: set DeletedAt (and UpdatedAt). Make sure the shared “load certificiranje” helper used by the other endpoints filters DeletedAt IS NULL.
  • Status check with the shared status helper → 409. Unknown or already deleted id → 404. Success → 204.

3. Verify implementation against spec

  • Status codes and the error value match deleteBtcCertification.
  • Active / past certificiranje → 409. BTC evaluator → 403. Deleting twice → 404 the second time.
  • After deleting: list excludes it; detail, oral exam form and results → 404.

Done when

A trainer can delete an upcoming certificiranje, and it disappears everywhere in the app while its data stays in the database.