Commit 36f285a2 by Tuukka Kivilahti

made some stuff for placequeue, some fixes also

1 parent fc4dfef2
......@@ -56,6 +56,8 @@ public interface PlaceBeanLocal {
boolean releasePlace(Place place);
boolean userReleasePlace(Place place);
Place mergeChanges(Place place);
PlaceGroup buySelectedPlaces(EventUser user) throws BortalCatchableException;
......
......@@ -7,6 +7,7 @@ import javax.ejb.Local;
import fi.codecrew.moya.model.EventMap;
import fi.codecrew.moya.model.EventUser;
import fi.codecrew.moya.model.MapQueueRules;
import fi.codecrew.moya.model.map.MapReservationQueueEntry;
@Local
......@@ -26,6 +27,10 @@ public interface QueueBeanLocal {
MapQueueI getMapQueue(EventMap map);
void saveQueue(MapQueueI queue, EventMap map);
MapQueueRules findRules(EventMap map);
public interface MapQueueI {
Collection<MapReservationQueueEntry> getQueue();
......
......@@ -417,7 +417,6 @@ public class BootstrapBean implements BootstrapBeanLocal {
});
dbUpdates.add(new String[] {
"ALTER TABLE products ADD COLUMN min_buy_count INTEGER default 0;"
});
......@@ -426,6 +425,11 @@ public class BootstrapBean implements BootstrapBeanLocal {
"ALTER TABLE discounts ADD COLUMN sort INTEGER NOT NULL default 10;"
});
dbUpdates.add(new String[] {
"CREATE TABLE map_queue_rules (id SERIAL NOT NULL, meta json, reserving_size INTEGER, default_timeout_min INTEGER, minium_slots_in_queue INTEGER, map_id INTEGER NOT NULL, PRIMARY KEY (id))",
"ALTER TABLE map_queue_rules ADD CONSTRAINT FK_map_queue_rules_map_id FOREIGN KEY (map_id) REFERENCES maps (id)",
});
}
......
......@@ -494,6 +494,21 @@ public class PlaceBean implements PlaceBeanLocal {
return releasePlacePriv(place) != null;
}
/**
* User release place
*
* @param place The place to be released
* @return true when successfull, on any erroro false.
*/
@Override
@RolesAllowed({ MapPermission.S_BUY_PLACES, MapPermission.S_MANAGE_OTHERS })
public boolean userReleasePlace(Place place) {
place = placeFacade.reload(place);
PlaceSlot s = releasePlacePriv(place);
return (s == null || s.getUsed() == null);
}
private PlaceSlot releasePlacePriv(Place place) {
EventUser user = permbean.getCurrentUser();
if (place.getGroup() != null || place.getCurrentUser() == null
......
package fi.codecrew.moya.beans.map;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import fi.codecrew.moya.model.MapQueueRules;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -24,9 +19,11 @@ public class MapQueue implements MapQueueI {
private int defaultTimeoutMin = 10;
private int minimumSlotsInQueue = 1;
private int reservingSize = 5;
// private final Set<MapReservationQueueEntry> reserving = new HashSet<>();
private final Set<EventUser> reserving = Collections.newSetFromMap(new ConcurrentHashMap<>());
private final PriorityQueue<MapReservationQueueEntry> queue = new PriorityQueue<>();
//private final PriorityQueue<MapReservationQueueEntry> queue = new PriorityQueue<>();
private final List<MapReservationQueueEntry> queue = new ArrayList<>();
private final ConcurrentMap<EventUser, MapReservationQueueEntry> queEntries = new ConcurrentHashMap<>();
private final Integer mapId;
//private final Object reservingLock = new Object();
......@@ -35,10 +32,23 @@ public class MapQueue implements MapQueueI {
public MapQueue(EventMap map) {
this.mapId = map.getId();
}
public MapQueue(EventMap map, MapQueueRules rules) {
this(map);
if(rules != null) {
this.defaultTimeoutMin = rules.getDefaultTimeoutMin();
this.minimumSlotsInQueue = rules.getMinimumSlotsInQueue();
this.reservingSize = rules.getReservingSize();
}
}
void doHousekeeping() {
timeoutEntries();
void timeoutEntries() {
}
private void timeoutEntries() {
logger.info("Timeouting entries");
// give 10 seconds mercy ( and give us some time to go through all entries)
Date now = new Date(System.currentTimeMillis() + 1000 * 15);
......@@ -81,13 +91,17 @@ public class MapQueue implements MapQueueI {
private final AtomicBoolean modifyReservers = new AtomicBoolean(false);
private void checkReservingEntry() {
if (reserving.size() < getReservingSize() && modifyReservers.compareAndSet(false, true)) {
try {
if (reserving.size() < getReservingSize()) {
MapReservationQueueEntry queEntry = queue.poll();
if (reserving.size() < getReservingSize() && !queue.isEmpty()) {
MapReservationQueueEntry queEntry = queue.get(0);
queue.remove(0);
Collections.sort(queue);
if (queEntry != null) {
reserving.add(queEntry.getUser());
queEntry.setReservationTimeout(new Date(System.currentTimeMillis() + getDefaultTimeoutMin() * 60 * 1000));
......@@ -112,6 +126,8 @@ public class MapQueue implements MapQueueI {
while (queue.remove(entry)) {
}
}
Collections.sort(queue);
checkReservingEntry();
return ret;
......@@ -122,6 +138,8 @@ public class MapQueue implements MapQueueI {
if (!reserving.contains(user) && !queue.contains(ret)) {
queue.add(ret);
Collections.sort(queue);
// Check if the user can be put to reservation queue immediately
checkReservingEntry();
......@@ -195,7 +213,7 @@ public class MapQueue implements MapQueueI {
}
@Override
public PriorityQueue<MapReservationQueueEntry> getQueue() {
public List<MapReservationQueueEntry> getQueue() {
return queue;
}
......
......@@ -5,7 +5,6 @@ import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import javax.annotation.OverridingMethodsMustInvokeSuper;
import javax.annotation.security.DeclareRoles;
import javax.annotation.security.RolesAllowed;
import javax.ejb.Asynchronous;
......@@ -17,6 +16,8 @@ import javax.ejb.Singleton;
import javax.ejb.TransactionManagement;
import javax.ejb.TransactionManagementType;
import fi.codecrew.moya.facade.MapQueueRulesFacade;
import fi.codecrew.moya.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -25,10 +26,6 @@ import fi.codecrew.moya.beans.PermissionBeanLocal;
import fi.codecrew.moya.beans.PlaceBeanLocal;
import fi.codecrew.moya.enums.apps.MapPermission;
import fi.codecrew.moya.facade.PlaceSlotFacade;
import fi.codecrew.moya.model.EventMap;
import fi.codecrew.moya.model.EventUser;
import fi.codecrew.moya.model.LanEventPropertyKey;
import fi.codecrew.moya.model.PlaceSlot;
import fi.codecrew.moya.model.map.MapReservationQueueEntry;
/**
......@@ -62,6 +59,9 @@ public class QueueBean implements QueueBeanLocal {
@EJB
private PermissionBeanLocal permbean;
@EJB
private MapQueueRulesFacade mapQueueRulesFacade;
@Lock(LockType.READ)
@Override
public boolean isReserving(EventMap map, EventUser user) {
......@@ -101,7 +101,7 @@ public class QueueBean implements QueueBeanLocal {
try {
final long oldTime = nextReservingTimeoutCheck.get();
for (MapQueue m : mapqueues.values()) {
m.timeoutEntries();
m.doHousekeeping();
}
// DO housekeeping every 10 seconds.
......@@ -119,7 +119,9 @@ public class QueueBean implements QueueBeanLocal {
MapQueue ret = mapqueues.get(map.getId());
if (ret == null) {
ret = new MapQueue(map);
ret = new MapQueue(map, mapQueueRulesFacade.findByMap(map));
MapQueue nret = mapqueues.putIfAbsent(map.getId(), ret);
if (nret != null) {
ret = nret;
......@@ -231,6 +233,30 @@ public class QueueBean implements QueueBeanLocal {
}
@Override
public void saveQueue(MapQueueI queue, EventMap map) {
MapQueueRules rules = mapQueueRulesFacade.findByMap(map);
if(rules == null) {
rules = new MapQueueRules();
rules.setMap(map);
mapQueueRulesFacade.create(rules);
rules = mapQueueRulesFacade.reload(rules);
}
rules.setDefaultTimeoutMin(queue.getDefaultTimeoutMin());
rules.setMinimumSlotsInQueue(queue.getMinimumSlotsInQueue());
rules.setReservingSize(queue.getReservingSize());
mapQueueRulesFacade.merge(rules);
}
@Override
public MapQueueRules findRules(EventMap map) {
return mapQueueRulesFacade.findByMap(map);
}
@Override
@Lock(LockType.READ)
@RolesAllowed(MapPermission.S_MANAGE_MAPS)
public void forceRemove(EventMap e, EventUser u) {
......
/*
* Copyright Codecrew Ry
*
* All rights reserved.
*
* This license applies to any software containing a notice placed by the
* copyright holder. Such software is herein referred to as the Software.
* This license covers modification, distribution and use of the Software.
*
* Any distribution and use in source and binary forms, with or without
* modification is not permitted without explicit written permission from the
* copyright owner.
*
* A non-exclusive royalty-free right is granted to the copyright owner of the
* Software to use, modify and distribute all modifications to the Software in
* future versions of the Software.
*
*/
package fi.codecrew.moya.facade;
import fi.codecrew.moya.model.EventMap;
import fi.codecrew.moya.model.MapQueueRules;
import fi.codecrew.moya.model.MapQueueRules_;
import javax.ejb.LocalBean;
import javax.ejb.Stateless;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Root;
@Stateless
@LocalBean
public class MapQueueRulesFacade extends IntegerPkGenericFacade<MapQueueRules> {
public MapQueueRulesFacade() {
super(MapQueueRules.class);
}
public MapQueueRules findByMap(EventMap map) {
if(map == null) {
throw new NullPointerException("Map must be given to MapQueueRules.findByMap");
}
CriteriaBuilder cb = getEm().getCriteriaBuilder();
CriteriaQuery<MapQueueRules> cq = cb.createQuery(MapQueueRules.class);
Root<MapQueueRules> root = cq.from(MapQueueRules.class);
cq.where(cb.equal(root.get(MapQueueRules_.map), map));
return getSingleNullableResult(getEm().createQuery(cq).setMaxResults(1));
}
}
/*
* Copyright Codecrew Ry
*
* All rights reserved.
*
* This license applies to any software containing a notice placed by the
* copyright holder. Such software is herein referred to as the Software.
* This license covers modification, distribution and use of the Software.
*
* Any distribution and use in source and binary forms, with or without
* modification is not permitted without explicit written permission from the
* copyright owner.
*
* A non-exclusive royalty-free right is granted to the copyright owner of the
* Software to use, modify and distribute all modifications to the Software in
* future versions of the Software.
*
*/
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package fi.codecrew.moya.model;
import javax.persistence.*;
import java.awt.*;
import java.util.Calendar;
/**
*
*/
@Entity
@Table(name = "map_queue_rules")
public class MapQueueRules extends GenericEntity {
private static final long serialVersionUID = 1L;
@Column(name = "reserving_size")
private Integer reservingSize = 5;
@Column(name = "default_timeout_min")
private Integer defaultTimeoutMin = 10;
@Column(name = "minium_slots_in_queue")
private Integer minimumSlotsInQueue = 1;
@JoinColumn(name = "map_id", referencedColumnName = "id")
@ManyToOne
private EventMap map;
public MapQueueRules() {
super();
}
public Integer getReservingSize() {
return reservingSize;
}
public void setReservingSize(Integer reservingSize) {
this.reservingSize = reservingSize;
}
public Integer getDefaultTimeoutMin() {
return defaultTimeoutMin;
}
public void setDefaultTimeoutMin(Integer defaultTimeoutMin) {
this.defaultTimeoutMin = defaultTimeoutMin;
}
public Integer getMinimumSlotsInQueue() {
return minimumSlotsInQueue;
}
public void setMinimumSlotsInQueue(Integer minimumSlotsInQueue) {
this.minimumSlotsInQueue = minimumSlotsInQueue;
}
public EventMap getMap() {
return map;
}
public void setMap(EventMap map) {
this.map = map;
}
}
......@@ -92,9 +92,17 @@ public class MapReservationQueueEntry extends GenericEntity implements Comparabl
}
@Override
/**
* Smaller is bigger.
*
* Bigger first.
*/
public int compareTo(MapReservationQueueEntry o) {
return getPlaceslotcount().compareTo(o.getPlaceslotcount());
if(o == null || o.getPlaceslotcount() == null)
return -1;
return o.getPlaceslotcount().compareTo(getPlaceslotcount());
}
public Integer getPlaceslotcount() {
......
......@@ -14,17 +14,22 @@
<ui:define name="content">
<h2>Queue properties</h2>
<h:form>
<p:panelGrid columns="2">
<h3><h:outputText value="#{i18n['queuemgmt.biggestIsFirst']}" /> </h3>
<p:panelGrid columns="3" id="queueGrid">
<p:outputLabel for="minslots" value="#{i18n['queuemgmt.minimumSlotsInQueue']}" />
<p:inputText id="minslots" value="#{queueManageView.queue.minimumSlotsInQueue}" />
<h:outputText value="#{i18n['queuemgmt.inDatabase']}: #{queueManageView.defaultValues.minimumSlotsInQueue}" />
<p:outputLabel for="reservingsize" value="#{i18n['queuemgmt.reservingSize']}" />
<p:inputText id="reservingsize" value="#{queueManageView.queue.reservingSize}" />
<h:outputText value="#{i18n['queuemgmt.inDatabase']}: #{queueManageView.defaultValues.reservingSize}" />
<p:outputLabel for="defaultTimeout" value="#{i18n['queuemgmt.defaultTimeoutMin']}" />
<p:inputText id="defaultTimeout" value="#{queueManageView.queue.defaultTimeoutMin}" />
<h:outputText value="#{i18n['queuemgmt.inDatabase']}: #{queueManageView.defaultValues.defaultTimeoutMin}" />
</p:panelGrid>
<p:commandButton ajax="false" value="#{i18n['queuemgmt.saveProperties']}"></p:commandButton>
<p:commandButton value="#{i18n['queuemgmt.saveProperties']}" actionListener="#{queueManageView.saveToDatabase}" update="queueGrid" ></p:commandButton>
</h:form>
......
......@@ -70,18 +70,22 @@
</p:column>
</p:dataTable>
<br /><br />
<ui:fragment rendered="#{ajaxMapView.reserving}">
<div style="margin: 5px;">
<h:form id="placeselectform">
<p:commandButton onclick="$(window).unbind('beforeunload');" rendered="#{ajaxMapView.canUserBuy()}" value="#{i18n['mapView.buyPlaces']}" actionListener="#{ajaxMapView.buySelectedPlaces()}" />
</h:form>
</div>
<h:outputText value="#{i18n['mapView.reserveTimeLeft']}: "/>
<span id="reserveTimeLeft"/><br/>
</ui:fragment>
<!-- Print queue status -->
<ui:fragment rendered="#{ajaxMapView.queueEnabled and not ajaxMapView.reserving}">
<h3><h:outputText value="#{i18n['mapView.youAreInQueue']}"/></h3>
<h4><h:outputText value="#{i18n['queuemgmt.biggestIsFirst']}" /> </h4>
<div style="margin: 1em;">
<h:outputText value="#{i18n['mapView.queuePosition']}: "/>
......@@ -98,6 +102,7 @@
<script type="text/javascript">
// Queue is enabled and we are reserving.
var queueReserving = #{(ajaxMapView.queueEnabled and not ajaxMapView.reserving)?"true":"false"};
var queueSelectingPlaces = #{(ajaxMapView.queueEnabled and ajaxMapView.reserving)?"true":"false"};
function updateMap() {
......@@ -114,6 +119,23 @@
}).fail(function () {
location.reload();
});
} else if(queueSelectingPlaces) {
$.getJSON("#{request.contextPath}/rest/placemap/v1/queueStillReserving/#{ajaxMapView.map.id}/#{ajaxMapView.eventuser.id}")
.done(function (data) {
if(data.value == 0) {
alert("#{i18n['reservequeue.reservingTimeIsUpAlert']}");
$(window).unbind('beforeunload');
location.href = "/";
}
var secsLeft = (#{ajaxMapView.reservingTimeslotEndtime}/1000) - ((new Date()).getTime()/1000 );
$("#reserveTimeLeft").text( Math.floor(secsLeft/60) + ":" + Math.round(secsLeft - (60 * Math.floor(secsLeft/60) ) ) );
}).fail(function () {
location.reload();
});
}
}
......@@ -130,7 +152,6 @@
location.reload();
} else {
$("#queuepos").text(data.value);
var d = new Date();
$("#queueupdated").text(new Date().toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, "$1"));
}
}
......
......@@ -181,6 +181,29 @@ public class PlacemapRestViewV1 {
return root;
}
@GET
@Path("/queueStillReserving/{mapid}/{userid}")
public IntegerRoot getStillReserving(@PathParam("mapid") Integer mapid, @PathParam("userid") Integer userid) {
EventUser user = null;
if (userid != null) {
user = userbean.findByEventUserId(userid);
} else {
user = permbean.getCurrentUser();
}
if (user == null) {
return null;
}
EventMap map = placebean.findMap(mapid);
IntegerRoot root = new IntegerRoot();
root.setValue(quebean.isReserving(map, user)?1:0);
return root;
}
@POST
@Path("/place/{place}")
public Response togglePlaceReservation(@PathParam("place") Integer placeId)
......@@ -200,7 +223,7 @@ public class PlacemapRestViewV1 {
boolean success = false;
if (p.isReservedFor(user)) {
success = placebean.releasePlace(p);
success = placebean.userReleasePlace(p);
} else if (p.isBuyable() && !p.isTaken()) {
logger.info("Rest Reserving place for place {}", p);
success = placebean.reservePlace(p, user);
......@@ -209,7 +232,7 @@ public class PlacemapRestViewV1 {
ResponseBuilder resp = null;
if (success) {
p = placebean.find(placeId);
List<Place> thisplace = new ArrayList<Place>();
List<Place> thisplace = new ArrayList<>();
thisplace.add(p);
resp = Response.ok(PojoUtils.parseSimplePlaces(thisplace, user, permbean.hasPermission(UserPermission.VIEW_ALL)));
} else {
......@@ -257,7 +280,7 @@ public class PlacemapRestViewV1 {
}
private ArrayList<PlaceCodePojo> makePlaceCodePojos(List<Place> places) {
ArrayList<PlaceCodePojo> ret = new ArrayList<PlaceCodePojo>();
ArrayList<PlaceCodePojo> ret = new ArrayList<>();
for (Place p : places) {
if (p.getCode() == null || p.getCode().isEmpty()) {
String newcode = null;
......
package fi.codecrew.moya.web.cdiview.map;
import java.util.Collection;
import java.util.HashMap;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.Map;
import javax.ejb.EJB;
import javax.enterprise.context.RequestScoped;
......@@ -232,6 +231,16 @@ public class AjaxMapView extends GenericCDIView {
return reserving;
}
public long getReservingTimeslotEndtime() {
Date timeout = quebean.getReservationTimeout(initMap());
if(timeout == null)
return 0;
return timeout.getTime();
}
public boolean canUserBuy() {
return permbean.hasPermission(MapPermission.BUY_PLACES) && (permbean.hasPermission(MapPermission.MANAGE_OTHERS)
|| quebean.isReserving(initMap(), userview.getSelectedUser()));
......
......@@ -9,6 +9,7 @@ import javax.enterprise.context.ConversationScoped;
import javax.faces.model.ListDataModel;
import javax.inject.Named;
import fi.codecrew.moya.model.MapQueueRules;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
......@@ -38,6 +39,8 @@ public class QueueManageView extends GenericCDIView {
private EventMap map;
private MapQueueRules rules;
private fi.codecrew.moya.beans.map.QueueBeanLocal.MapQueueI queue;
private String username;
......@@ -59,6 +62,8 @@ public class QueueManageView extends GenericCDIView {
if (super.requirePermissions(MapPermission.MANAGE_MAPS) && map == null) {
map = placebean.findMap(mapId);
rules = quebean.findRules(map);
queue = quebean.getMapQueue(map);
logger.info("Got queue {} for map {} with id {} to manage", queue, map, mapId);
super.beginConversation();
......@@ -154,4 +159,13 @@ public class QueueManageView extends GenericCDIView {
public void setTime(Date time) {
this.time = time;
}
public void saveToDatabase() {
quebean.saveQueue(queue, map);
rules = quebean.findRules(map); // refresh
}
public MapQueueRules getDefaultValues() {
return rules;
}
}
......@@ -899,7 +899,7 @@ queuemgmt.defaultTimeoutMin = Reservation time
queuemgmt.minimumSlotsInQueue = Minimum amount of places to enter queue
queuemgmt.remove = Remove user from queue
queuemgmt.reservingSize = Number of people reserving at the same time
queuemgmt.saveProperties = Save properties
queuemgmt.saveProperties = Save properties to Database
queuemgmt.time = Reservation time
queuemgmt.username = Login
......@@ -1582,3 +1582,8 @@ yes = Yes
page.product.shopClosed.header=Shop is closed!
page.product.shopClosed.notOpenYet=Shop is not opened. Try again later.
page.product.shopClosed.alreadyClosed=Shop is closed, welcome back in next event!
queuemgmt.inDatabase=Value in database
queuemgmt.biggestFirst=Biggest group first
queuemgmt.biggestIsFirst=Bigger group automaticly rises up in queue.
reservequeue.reservingTimeIsUpAlert=Timeslot for your place reservation has timeouted. Pleace go back to queue to continue.
mapView.reserveTimeLeft=Time to reserve places
......@@ -1163,7 +1163,7 @@ queuemgmt.defaultTimeoutMin = Reservation time
queuemgmt.minimumSlotsInQueue = Minimum amount of places to enter queue
queuemgmt.remove = Remove user from queue
queuemgmt.reservingSize = Number of people reserving at the same time
queuemgmt.saveProperties = Save properties
queuemgmt.saveProperties = Save properties to Database
queuemgmt.time = Reservation time
queuemgmt.username = Login
......@@ -1860,3 +1860,8 @@ yes = Yes
page.product.shopClosed.header=Shop is closed!
page.product.shopClosed.notOpenYet=Shop is not opened. Try again later.
page.product.shopClosed.alreadyClosed=Shop is closed, welcome back in next event!
queuemgmt.inDatabase=Value in database
queuemgmt.biggestFirst=Biggest group first
queuemgmt.biggestIsFirst=Bigger group automaticly rises up in queue.
reservequeue.reservingTimeIsUpAlert=Timeslot for your place reservation has timeouted. Pleace go back to queue to continue.
mapView.reserveTimeLeft=Time to reserve places
......@@ -1150,7 +1150,7 @@ queuemgmt.defaultTimeoutMin = Varausaika
queuemgmt.minimumSlotsInQueue = Jonoonp\u00E4\u00E4syn v\u00E4himm\u00E4ispaikkam\u00E4\u00E4r\u00E4
queuemgmt.remove = Poista k\u00E4ytt\u00E4j\u00E4 jonosta
queuemgmt.reservingSize = Samanaikaisesti varaamaan p\u00E4\u00E4sevien m\u00E4\u00E4r\u00E4
queuemgmt.saveProperties = Tallenna arvot
queuemgmt.saveProperties = Tallenna arvot tietokantaan
queuemgmt.time = Varausaika
queuemgmt.username = K\u00E4ytt\u00E4j\u00E4tunnus
......@@ -1847,3 +1847,8 @@ yes = Kyll\u00E4
page.product.shopClosed.header=Kauppa on kiinni!
page.product.shopClosed.notOpenYet=Tapahtuman lipunmyynti ei ole viel\u00E4 alkanut, Kokeile uudelleen my\u00F6hemmin.
page.product.shopClosed.alreadyClosed=Kauppa on suljettu, tervetuloa uudelleen tuleviin tapahtumiin.
queuemgmt.inDatabase=Arvo tietokannassa
queuemgmt.biggestFirst=Isoin ryhm\u00E4 ensin
queuemgmt.biggestIsFirst=Isompi ryhm\u00E4 nousee automaattisesti jonossa edemm\u00E4ksi.
reservequeue.reservingTimeIsUpAlert=Sinulle varattu varausauka on kulunut loppuun! Palaa takaisin 'varaa paikkasi' -sivulle jatkaaksesi.
mapView.reserveTimeLeft=Aikaa varata paikkasi
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!