19.0 vanilla
|
|
@ -0,0 +1,141 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { BarcodeScanner } from "@barcodes/components/barcode_scanner";
|
||||
import { Component, onWillStart } from "@odoo/owl";
|
||||
import { isDisplayStandalone } from "@web/core/browser/feature_detection";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useBus, useService } from "@web/core/utils/hooks";
|
||||
import { url } from '@web/core/utils/urls';
|
||||
import { EventRegistrationSummaryDialog } from "./event_registration_summary_dialog";
|
||||
import { scanBarcode } from "@web/core/barcode/barcode_dialog";
|
||||
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
|
||||
|
||||
export class EventScanView extends Component {
|
||||
static template = "event.EventScanView";
|
||||
static components = { BarcodeScanner };
|
||||
static props = { ...standardActionServiceProps };
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.dialog = useService("dialog");
|
||||
this.notification = useService("notification");
|
||||
this.orm = useService("orm");
|
||||
|
||||
const { default_event_id, active_model, active_id } = this.props.action.context;
|
||||
this.eventId = default_event_id || (active_model === "event.event" && active_id);
|
||||
this.isMultiEvent = !this.eventId;
|
||||
this.isDisplayStandalone = isDisplayStandalone();
|
||||
|
||||
const barcode = useService("barcode");
|
||||
useBus(barcode.bus, "barcode_scanned", (ev) => this.onBarcodeScanned(ev.detail.barcode));
|
||||
|
||||
onWillStart(this.onWillStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Fetch barcode init information. Notably eventId triggers mono- or multi-
|
||||
* event mode (Registration Desk in multi event allow to manage attendees
|
||||
* from several events and tickets without reloading / changing event in UX.
|
||||
*/
|
||||
async onWillStart() {
|
||||
this.data = await rpc("/event/init_barcode_interface", {
|
||||
event_id: this.eventId,
|
||||
});
|
||||
const fileExtension = new Audio().canPlayType("audio/ogg") ? "ogg" : "mp3";
|
||||
this.sounds = {
|
||||
error: new Audio(url(`/barcodes/static/src/audio/error.${fileExtension}`)),
|
||||
notify: new Audio(url(`/mail/static/src/audio/ting.${fileExtension}`)),
|
||||
};
|
||||
this.sounds.error.load();
|
||||
this.sounds.notify.load();
|
||||
}
|
||||
|
||||
playSound(type) {
|
||||
type = type || "notify";
|
||||
this.sounds[type].currentTime = 0;
|
||||
this.sounds[type].play();
|
||||
}
|
||||
|
||||
/**
|
||||
* When scanning a barcode, call Registration.register_attendee() to get
|
||||
* formatted registration information, notably its status or event-related
|
||||
* information. Open a confirmation / choice Dialog to confirm attendee.
|
||||
* @param {Object} barcode
|
||||
* @param {function} onNextScanTriggered
|
||||
*/
|
||||
async onBarcodeScanned(barcode, onNextScanTriggered = () => {}) {
|
||||
const result = await this.orm.call("event.registration", "register_attendee", [], {
|
||||
barcode: barcode,
|
||||
event_id: this.eventId,
|
||||
});
|
||||
|
||||
if (result.error && result.error === "invalid_ticket") {
|
||||
this.playSound("error");
|
||||
this.notification.add(_t("Invalid ticket"), {
|
||||
type: "danger",
|
||||
});
|
||||
} else {
|
||||
this.registrationId = result.id;
|
||||
this.closeLastDialog?.();
|
||||
this.closeLastDialog = this.dialog.add(
|
||||
EventRegistrationSummaryDialog,
|
||||
{
|
||||
playSound: (type) => this.playSound(type),
|
||||
doNextScan: onNextScanTriggered,
|
||||
registration: result
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplication of the openMobileScanner() method from BarcodeScanner component
|
||||
* to avoid using the component in the template and to be able to call it directly
|
||||
* from the dialog.
|
||||
*/
|
||||
async doNextScan() {
|
||||
let error = null;
|
||||
let barcode = null;
|
||||
try {
|
||||
barcode = await scanBarcode(this.env, this.facingMode);
|
||||
} catch (err) {
|
||||
error = err.message;
|
||||
}
|
||||
|
||||
if (barcode) {
|
||||
await this.onBarcodeScanned(barcode, this.doNextScan.bind(this));
|
||||
if ("vibrate" in window.navigator) {
|
||||
window.navigator.vibrate(100);
|
||||
}
|
||||
} else {
|
||||
this.notification.add(error || _t("Please, Scan again!"), {
|
||||
type: "warning",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickSelectAttendee() {
|
||||
if (this.isMultiEvent) {
|
||||
this.actionService.doAction("event.event_registration_action");
|
||||
} else {
|
||||
this.actionService.doAction("event.event_registration_action_kanban", {
|
||||
additionalContext: {
|
||||
active_id: this.eventId,
|
||||
search_default_unconfirmed: true,
|
||||
search_default_confirmed: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onClickBackToEvents() {
|
||||
if (this.isMultiEvent) {
|
||||
this.actionService.doAction("event.action_event_view", { clearBreadcrumbs: true });
|
||||
} else {
|
||||
this.actionService.restore();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("event.event_barcode_scan_view", EventScanView);
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
.o_event_barcode_bg {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
@include o-position-absolute(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.o_event_barcode_main {
|
||||
font-family: 'Lato', sans-serif;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: 2em;
|
||||
img .o_event_barcode_image {
|
||||
width: 115px;
|
||||
height: 60px;
|
||||
}
|
||||
.o_event_barcode_company_image {
|
||||
overflow: hidden;
|
||||
margin: 1rem 0 2rem;
|
||||
max-width: 200px;
|
||||
max-height: 100px;
|
||||
}
|
||||
.o_barcode_mobile_container {
|
||||
margin-top: 40px;
|
||||
margin-bottom: -40px;
|
||||
img {
|
||||
height: 185px;
|
||||
width: 275px;
|
||||
}
|
||||
// In order to have the o_mobile_barcode button on both the image and the label,
|
||||
// We use negative margin at the bottom and 0 opacity (since not needed in the view)
|
||||
.o_mobile_barcode {
|
||||
opacity: 0;
|
||||
height: 225px;
|
||||
width: 275px;
|
||||
bottom: -40px;
|
||||
}
|
||||
.o_barcode_laser {
|
||||
height: 3px;
|
||||
width: 125%;
|
||||
left: -12.5%;
|
||||
}
|
||||
}
|
||||
@include media-breakpoint-down(md) {
|
||||
padding: 0 1em 1em .75em;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
flex: 0 0 auto;
|
||||
width: 550px;
|
||||
border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.6);
|
||||
font-size: 1.2em;
|
||||
padding: 0 1em 1em .75em;
|
||||
}
|
||||
}
|
||||
.o_notification_manager {
|
||||
.o_event_success {
|
||||
color: white;
|
||||
background-color: #5BC236;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="event.EventScanView">
|
||||
<div class="o_event_barcode_bg o_home_menu_background">
|
||||
<div class="o_event_barcode_main container d-flex flex-column h-100 h-sm-auto bg-view shadow">
|
||||
<div class="d-flex align-items-center justify-content-between my-3">
|
||||
<a t-if="!isDisplayStandalone" href="#" class="o_event_previous_menu float-start"><i class="oi oi-chevron-left fa-lg" t-on-click.prevent="() => this.onClickBackToEvents()"></i></a>
|
||||
<span class="fs-2 me-auto ms-2" t-out="data.name"/>
|
||||
<a t-if="!isDisplayStandalone" class="btn btn-secondary d-flex align-items-center justify-content-center fw-bolder" href="/scoped_app?app_id=event&path=scoped_app/registration-desk" target="_blank">Install</a>
|
||||
</div>
|
||||
<div class="flex-grow-1 d-flex flex-column justify-content-center align-items-center vh-50">
|
||||
<BarcodeScanner onBarcodeScanned="(ev) => this.onBarcodeScanned(ev, doNextScan.bind(this))"/>
|
||||
<div class="my-5 text-center">
|
||||
<h5 class="mt8 text-muted">Scan or Tap</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="o_event_select_attendee btn btn-primary w-100" t-on-click="() => this.onClickSelectAttendee()">
|
||||
<div class="fw-bolder mb16 mt16">Select Attendee</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import { Component, onMounted, useState, useRef } from "@odoo/owl";
|
||||
import { isBarcodeScannerSupported } from "@web/core/barcode/barcode_video_scanner";
|
||||
import { Dialog } from "@web/core/dialog/dialog";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class EventRegistrationSummaryDialog extends Component {
|
||||
static template = "event.EventRegistrationSummaryDialog";
|
||||
static components = { Dialog };
|
||||
static props = {
|
||||
close: Function,
|
||||
doNextScan: { type: Function, optional: true },
|
||||
model: { type: Object, optional: true },
|
||||
playSound: { type: Function, optional: true },
|
||||
registration: { type: Object },
|
||||
};
|
||||
|
||||
setup() {
|
||||
this.actionService = useService("action");
|
||||
this.isBarcodeScannerSupported = isBarcodeScannerSupported();
|
||||
this.orm = useService("orm");
|
||||
this.notification = useService("notification");
|
||||
this.continueButtonRef = useRef("continueButton");
|
||||
this.button = useState({enabled: true});
|
||||
|
||||
this.registrationStatus = useState({value: this.registration.status});
|
||||
|
||||
onMounted(() => {
|
||||
if (['already_registered', 'need_manual_confirmation'].includes(this.props.registration.status) && this.props.playSound) {
|
||||
this.props.playSound("notify");
|
||||
} else if (['not_ongoing_event', 'canceled_registration'].includes(this.props.registration.status) && this.props.playSound) {
|
||||
this.props.playSound("error");
|
||||
}
|
||||
// Without this, repeat barcode scans don't work as focus is lost
|
||||
this.continueButtonRef.el?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
get registration() {
|
||||
return this.props.registration;
|
||||
}
|
||||
|
||||
get needManualConfirmation() {
|
||||
return this.registrationStatus.value === "need_manual_confirmation";
|
||||
}
|
||||
|
||||
async onRegistrationConfirm() {
|
||||
if (this.registrationStatus.value !== "confirmed_registration") {
|
||||
this.button.enabled = false
|
||||
await this.orm.call("event.registration", "action_set_done", [this.registration.id]).catch(() => this.button.enabled = true);
|
||||
this.registrationStatus.value = "confirmed_registration";
|
||||
}
|
||||
this.props.close();
|
||||
if (this.props.model) {
|
||||
this.props.model.load();
|
||||
}
|
||||
if (this.props.doNextScan) {
|
||||
this.onScanNext();
|
||||
}
|
||||
}
|
||||
|
||||
async undoRegistration() {
|
||||
if (["confirmed_registration", "already_registered"].includes(this.registrationStatus.value)) {
|
||||
await this.orm.call("event.registration", "action_confirm", [this.registration.id]);
|
||||
} else if (this.registrationStatus.value == "unconfirmed_registration") {
|
||||
await this.orm.call("event.registration", "action_set_draft", [this.registration.id]);
|
||||
}
|
||||
this.props.close();
|
||||
if (this.props.model) {
|
||||
this.props.model.load();
|
||||
}
|
||||
}
|
||||
|
||||
async onRegistrationPrintPdf() {
|
||||
await this.actionService.doAction({
|
||||
type: "ir.actions.report",
|
||||
report_type: "qweb-pdf",
|
||||
report_name: `event.event_registration_report_template_badge/${this.registration.id}`,
|
||||
});
|
||||
if (this.props.doNextScan) {
|
||||
this.onScanNext();
|
||||
}
|
||||
}
|
||||
|
||||
async onRegistrationView() {
|
||||
await this.actionService.doAction({
|
||||
type: "ir.actions.act_window",
|
||||
res_model: "event.registration",
|
||||
res_id: this.registration.id,
|
||||
views: [[false, "form"]],
|
||||
target: "current",
|
||||
});
|
||||
this.props.close();
|
||||
}
|
||||
|
||||
async onScanNext() {
|
||||
this.props.close();
|
||||
if (this.isBarcodeScannerSupported) {
|
||||
this.props.doNextScan();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="event.EventRegistrationSummaryDialog" >
|
||||
<Dialog size="'md'" title.translate="Home">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 w-100 fs-2">
|
||||
<div t-if="['confirmed_registration', 'unconfirmed_registration'].includes(registrationStatus.value)" class="alert alert-success d-flex justify-content-center" role="alert">
|
||||
<i class="fa fa-check-circle align-self-center me-2 ms-0 ms-sm-5"/>
|
||||
<span>Successfully registered!</span>
|
||||
<button type="button" class="btn btn-link ms-3 ms-sm-5" t-on-click="undoRegistration">
|
||||
Undo
|
||||
</button>
|
||||
</div>
|
||||
<div t-else="" class="alert alert-warning d-flex justify-content-center" role="alert">
|
||||
<i class="fa fa-exclamation-circle me-2 align-self-center ms-0 ms-sm-5"/>
|
||||
<t t-if="registrationStatus.value === 'need_manual_confirmation'">
|
||||
<span>This ticket is for another event!<br/>
|
||||
Confirm attendance?</span>
|
||||
</t>
|
||||
<t t-elif="registrationStatus.value === 'not_ongoing_event'">
|
||||
<span>This ticket is not for an ongoing event</span>
|
||||
</t>
|
||||
<t t-elif="registrationStatus.value === 'canceled_registration'">
|
||||
<span>Cancelled registration</span>
|
||||
</t>
|
||||
<t t-elif="registrationStatus.value == 'already_registered'">
|
||||
<t t-if="!registration.is_date_closed_today">
|
||||
<span>Ticket was first scanned on <t t-esc="registration.date_closed_formatted"/></span>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span>Ticket already scanned!</span>
|
||||
</t>
|
||||
</t>
|
||||
<button type="button" class="btn btn-link ms-3 ms-sm-5" t-on-click="undoRegistration">
|
||||
Undo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row fs-1">
|
||||
<div id="registration_header" class="col-lg-12 d-flex align-items-baseline gap-3">
|
||||
<t t-set="guest_label">Guest #</t>
|
||||
<t t-if="registration.name" t-out="registration.name"/>
|
||||
<t t-else="" t-out="guest_label + registration.id"/>
|
||||
</div>
|
||||
</div>
|
||||
<div id="registration_information" class="row mt-4">
|
||||
<div class="col-lg-12">
|
||||
<table class="table table-striped fs-4">
|
||||
<tr t-if="registration.company_name"><td>Company</td><td><t t-out="registration.company_name"/></td></tr>
|
||||
<tr><td>Event</td><td><t t-out="registration.event_display_name"/></td></tr>
|
||||
<tr t-if="registration.slot_name"><td>Slot</td><td><t t-out="registration.slot_name"/></td></tr>
|
||||
<tr t-if="registration.ticket_name"><td>Ticket Type</td><td><t t-out="registration.ticket_name"/></td></tr>
|
||||
<tr t-if="registration.registration_answers && registration.registration_answers.length > 0">
|
||||
<td class="d-flex">Answers</td>
|
||||
<td>
|
||||
<div class="d-flex flex-wrap p-0">
|
||||
<span t-foreach="registration.registration_answers" t-as="registration_answer" t-key="registration_answer_index"
|
||||
t-attf-class="o_tag o_tag_badge_text o_tag_color_#{registration_answer_index % 10} badge rounded-pill text-truncate p-1 me-1 mb-1"
|
||||
t-out="registration_answer"/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<t t-set-slot="footer">
|
||||
<button t-att-disabled="!button.enabled" t-ref="continueButton" class="btn btn-primary" t-on-click="() => this.onRegistrationConfirm()">Continue</button>
|
||||
<button id="print_button" class="btn btn-primary" t-on-click="() => this.onRegistrationPrintPdf()">Print</button>
|
||||
<button class="btn btn-secondary" t-on-click="() => this.onRegistrationView()">Edit</button>
|
||||
</t>
|
||||
</Dialog>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { formatSelection } from "@web/views/fields/formatters";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { StateSelectionField, stateSelectionField } from "@web/views/fields/state_selection/state_selection_field";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
|
||||
/**
|
||||
* This widget is used to enhance the Event State Selection field UI.
|
||||
* It extends `StateSelectionField` to provide visual feedback using icons
|
||||
* and color classes for different states: `normal`, `done`, `blocked`, `cancel`.
|
||||
*/
|
||||
export class EventStateSelection extends StateSelectionField {
|
||||
static template = "event.EventStateSelection";
|
||||
|
||||
setup() {
|
||||
this.dialog = useService("dialog");
|
||||
this.icons = {
|
||||
normal: "o_status",
|
||||
done: "o_status o_status_green",
|
||||
blocked: "fa fa-lg fa-exclamation-circle",
|
||||
cancel: "fa fa-lg fa-times-circle",
|
||||
};
|
||||
this.colorIcons = {
|
||||
normal: "",
|
||||
done: "text-success",
|
||||
blocked: "o_status_blocked",
|
||||
cancel: "text-danger",
|
||||
};
|
||||
}
|
||||
|
||||
get options() {
|
||||
return ["normal", "done", "blocked", "cancel"].map((state) => [state, new Map(super.options).get(state)]);
|
||||
}
|
||||
|
||||
get label() {
|
||||
return formatSelection(this.currentValue, { selection: this.options });
|
||||
}
|
||||
|
||||
stateIcon(value) {
|
||||
return this.icons[value] || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
statusColor(value) {
|
||||
return this.colorIcons[value] || "";
|
||||
}
|
||||
}
|
||||
|
||||
export const EventStateSelectionField = {
|
||||
...stateSelectionField,
|
||||
component: EventStateSelection,
|
||||
supportedOptions: [
|
||||
...stateSelectionField.supportedOptions
|
||||
]
|
||||
}
|
||||
|
||||
registry.category("fields").add("event_state_selection", EventStateSelectionField);
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
.o_field_event_state_selection {
|
||||
.o_status {
|
||||
width: $font-size-base * 1.36;
|
||||
height: $font-size-base * 1.36;
|
||||
text-align: center;
|
||||
margin-top: -0.5px;
|
||||
}
|
||||
|
||||
.fa-lg {
|
||||
font-size: 1.75em;
|
||||
margin-top: -2.5px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
}
|
||||
|
||||
.o_status_blocked {
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
.event_state_selection_menu {
|
||||
.fa {
|
||||
margin-top: -1.5px;
|
||||
font-size: 1.315em;
|
||||
vertical-align: -6%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.o_status {
|
||||
margin-top: 1px;
|
||||
width: 14.65px;
|
||||
height: 14.65px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.o_status_blocked {
|
||||
color: $warning;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<templates>
|
||||
<t t-name="event.EventStateSelection" t-inherit="web.StateSelectionField" t-inherit-mode="primary">
|
||||
<xpath expr="//Dropdown/button/div" position="replace">
|
||||
<div class="d-flex align-items-center">
|
||||
<i t-attf-class="{{ stateIcon(currentValue) }} {{ statusColor(currentValue) }} "/>
|
||||
</div>
|
||||
</xpath>
|
||||
<!-- Dropdown Selection-->
|
||||
<xpath expr="//Dropdown" position="attributes">
|
||||
<attribute name="menuClass" separator=" + " add="' event_state_selection_menu'"></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//CheckboxItem/span[1]" position="attributes">
|
||||
<attribute name="t-attf-class" separator=" " add="{{ stateIcon(option[0]) }}" remove="o_status"></attribute>
|
||||
</xpath>
|
||||
<xpath expr="//CheckboxItem/span[2]" position="attributes">
|
||||
<attribute name="t-attf-class">{{ statusColor(option[0]) }}</attribute>
|
||||
</xpath>
|
||||
<!-- Dropdown Divider -->
|
||||
<xpath expr="//CheckboxItem" position="before">
|
||||
<div t-if="option[0] == 'cancel'" role="separator" class="dropdown-divider"/>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
|
|
@ -1,30 +1,34 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import { registry } from "@web/core/registry";
|
||||
import { _lt } from "@web/core/l10n/translation";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { standardFieldProps } from "@web/views/fields/standard_field_props";
|
||||
|
||||
const { Component } = owl;
|
||||
import { Component } from "@odoo/owl";
|
||||
|
||||
export class IconSelectionField extends Component {
|
||||
static template = "event.IconSelectionField";
|
||||
static props = {
|
||||
...standardFieldProps,
|
||||
icons: Object,
|
||||
};
|
||||
|
||||
get icon() {
|
||||
return this.props.icons[this.props.value];
|
||||
return this.props.icons[this.props.record.data[this.props.name]];
|
||||
}
|
||||
get title() {
|
||||
return this.props.value.charAt(0).toUpperCase() + this.props.value.slice(1);
|
||||
return (
|
||||
this.props.record.data[this.props.name].charAt(0).toUpperCase() +
|
||||
this.props.record.data[this.props.name].slice(1)
|
||||
);
|
||||
}
|
||||
}
|
||||
IconSelectionField.template = "event.IconSelectionField";
|
||||
IconSelectionField.props = {
|
||||
...standardFieldProps,
|
||||
icons: Object,
|
||||
|
||||
export const iconSelectionField = {
|
||||
component: IconSelectionField,
|
||||
displayName: _t("Icon Selection"),
|
||||
supportedTypes: ["char", "text", "selection"],
|
||||
listViewWidth: ({ hasLabel }) => (!hasLabel ? 20 : false),
|
||||
extractProps: ({ options }) => ({
|
||||
icons: options,
|
||||
}),
|
||||
};
|
||||
|
||||
IconSelectionField.displayName = _lt("Icon Selection");
|
||||
IconSelectionField.supportedTypes = ["char", "text", "selection"];
|
||||
|
||||
IconSelectionField.extractProps = ({ attrs }) => ({
|
||||
icons: attrs.options,
|
||||
});
|
||||
|
||||
registry.category("fields").add("event_icon_selection", IconSelectionField);
|
||||
registry.category("fields").add("event_icon_selection", iconSelectionField);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="event.IconSelectionField" owl="1">
|
||||
<t t-name="event.IconSelectionField">
|
||||
<i t-att-class="icon" t-att-data-tooltip="title"/>
|
||||
</t>
|
||||
|
||||
|
|
|
|||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" height="24px" width="21px" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 22.773 22.773" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path d="M15.769,0c0.053,0,0.106,0,0.162,0c0.13,1.606-0.483,2.806-1.228,3.675c-0.731,0.863-1.732,1.7-3.351,1.573
|
||||
c-0.108-1.583,0.506-2.694,1.25-3.561C13.292,0.879,14.557,0.16,15.769,0z"/>
|
||||
<path d="M20.67,16.716c0,0.016,0,0.03,0,0.045c-0.455,1.378-1.104,2.559-1.896,3.655c-0.723,0.995-1.609,2.334-3.191,2.334
|
||||
c-1.367,0-2.275-0.879-3.676-0.903c-1.482-0.024-2.297,0.735-3.652,0.926c-0.155,0-0.31,0-0.462,0
|
||||
c-0.995-0.144-1.798-0.932-2.383-1.642c-1.725-2.098-3.058-4.808-3.306-8.276c0-0.34,0-0.679,0-1.019
|
||||
c0.105-2.482,1.311-4.5,2.914-5.478c0.846-0.52,2.009-0.963,3.304-0.765c0.555,0.086,1.122,0.276,1.619,0.464
|
||||
c0.471,0.181,1.06,0.502,1.618,0.485c0.378-0.011,0.754-0.208,1.135-0.347c1.116-0.403,2.21-0.865,3.652-0.648
|
||||
c1.733,0.262,2.963,1.032,3.723,2.22c-1.466,0.933-2.625,2.339-2.427,4.74C17.818,14.688,19.086,15.964,20.67,16.716z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.1074 6.5H7.10742V17.5H18.1074V6.5Z" fill="white"/>
|
||||
<path d="M13.4473 10.46L13.9513 11.18L14.7433 10.604V14.78H15.6073V9.308H14.8873L13.4473 10.46Z" fill="#1E88E5"/>
|
||||
<path d="M12.0789 11.8725C12.3914 11.5855 12.5854 11.1875 12.5854 10.748C12.5854 9.8745 11.8189 9.164 10.8769 9.164C10.0759 9.164 9.39092 9.6685 9.21192 10.3905L10.0404 10.601C10.1229 10.269 10.4744 10.028 10.8769 10.028C11.3479 10.028 11.7314 10.351 11.7314 10.748C11.7314 11.145 11.3479 11.468 10.8769 11.468H10.3784V12.332H10.8769C11.4174 12.332 11.8734 12.7075 11.8734 13.152C11.8734 13.604 11.4404 13.972 10.9079 13.972C10.4269 13.972 10.0159 13.667 9.95092 13.263L9.10742 13.401C9.23842 14.219 10.0124 14.836 10.9074 14.836C11.9109 14.836 12.7274 14.0805 12.7274 13.152C12.7274 12.6405 12.4754 12.1815 12.0789 11.8725Z" fill="#1E88E5"/>
|
||||
<path d="M17.6074 21H7.60742L7.10742 19L7.60742 17H17.6074L18.1074 19L17.6074 21Z" fill="#FBC02D"/>
|
||||
<path d="M19.6074 17.5L21.6074 17V7L19.6074 6.5L17.6074 7V17L19.6074 17.5Z" fill="#4CAF50"/>
|
||||
<path d="M17.6074 7L18.1074 5L17.6074 3H5.10742C4.27892 3 3.60742 3.6715 3.60742 4.5V17L5.60742 17.5L7.60742 17V7H17.6074Z" fill="#1E88E5"/>
|
||||
<path d="M17.6074 17V21L21.6074 17H17.6074Z" fill="#E53935"/>
|
||||
<path d="M20.1074 3H17.6074V7H21.6074V4.5C21.6074 3.6715 20.9359 3 20.1074 3Z" fill="#1565C0"/>
|
||||
<path d="M5.10742 21H7.60742V17H3.60742V19.5C3.60742 20.3285 4.27892 21 5.10742 21Z" fill="#1565C0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -0,0 +1,20 @@
|
|||
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.8931 6.5H22.1596C22.5646 6.5 22.8931 6.8285 22.8931 7.2335V16.7665C22.8931 17.1715 22.5646 17.5 22.1596 17.5H14.8931V6.5Z" fill="#1976D2"/>
|
||||
<path d="M21.8931 8.979H14.8931V16.75H21.8931V8.979Z" fill="white"/>
|
||||
<path d="M14.3931 22L2.89307 19.75V4.25L14.3931 2V22Z" fill="#1976D2"/>
|
||||
<path d="M8.51807 8.25C6.93007 8.25 5.64307 9.929 5.64307 12C5.64307 14.071 6.93007 15.75 8.51807 15.75C10.1061 15.75 11.3931 14.071 11.3931 12C11.3931 9.929 10.1061 8.25 8.51807 8.25ZM8.39307 14.25C7.56457 14.25 6.89307 13.2425 6.89307 12C6.89307 10.7575 7.56457 9.75 8.39307 9.75C9.22157 9.75 9.89307 10.7575 9.89307 12C9.89307 13.2425 9.22157 14.25 8.39307 14.25Z" fill="white"/>
|
||||
<path d="M16.2665 14.8685H14.9165V16.3185H16.2665V14.8685Z" fill="#1976D2"/>
|
||||
<path d="M17.9672 14.8685H16.6172V16.3185H17.9672V14.8685Z" fill="#1976D2"/>
|
||||
<path d="M19.6674 14.8685H18.3174V16.3185H19.6674V14.8685Z" fill="#1976D2"/>
|
||||
<path d="M16.2665 13.0795H14.9165V14.5295H16.2665V13.0795Z" fill="#1976D2"/>
|
||||
<path d="M17.9672 13.0795H16.6172V14.5295H17.9672V13.0795Z" fill="#1976D2"/>
|
||||
<path d="M19.6674 13.0795H18.3174V14.5295H19.6674V13.0795Z" fill="#1976D2"/>
|
||||
<path d="M21.3681 13.0795H20.0181V14.5295H21.3681V13.0795Z" fill="#1976D2"/>
|
||||
<path d="M16.2665 11.353H14.9165V12.803H16.2665V11.353Z" fill="#1976D2"/>
|
||||
<path d="M17.9672 11.353H16.6172V12.803H17.9672V11.353Z" fill="#1976D2"/>
|
||||
<path d="M19.6674 11.353H18.3174V12.803H19.6674V11.353Z" fill="#1976D2"/>
|
||||
<path d="M21.3681 11.353H20.0181V12.803H21.3681V11.353Z" fill="#1976D2"/>
|
||||
<path d="M17.9672 9.556H16.6172V11.006H17.9672V9.556Z" fill="#1976D2"/>
|
||||
<path d="M19.6674 9.556H18.3174V11.006H19.6674V9.556Z" fill="#1976D2"/>
|
||||
<path d="M21.3681 9.556H20.0181V11.006H21.3681V9.556Z" fill="#1976D2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 4.1 KiB |
|
|
@ -0,0 +1,9 @@
|
|||
class EventAdditionalTourSteps {
|
||||
|
||||
_get_website_event_steps() {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EventAdditionalTourSteps;
|
||||
|
|
@ -1,78 +1,68 @@
|
|||
odoo.define('event.event_steps', function (require) {
|
||||
"use strict";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { stepUtils } from "@web_tour/tour_utils";
|
||||
|
||||
var core = require('web.core');
|
||||
import EventAdditionalTourSteps from "@event/js/tours/event_steps";
|
||||
|
||||
var EventAdditionalTourSteps = core.Class.extend({
|
||||
import { markup } from "@odoo/owl";
|
||||
|
||||
_get_website_event_steps: function () {
|
||||
return [false];
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
return EventAdditionalTourSteps;
|
||||
|
||||
});
|
||||
|
||||
odoo.define('event.event_tour', function (require) {
|
||||
"use strict";
|
||||
|
||||
const {_t} = require('web.core');
|
||||
const {Markup} = require('web.utils');
|
||||
|
||||
var tour = require('web_tour.tour');
|
||||
var EventAdditionalTourSteps = require('event.event_steps');
|
||||
|
||||
tour.register('event_tour', {
|
||||
url: '/web',
|
||||
rainbowManMessage: _t("Great! Now all you have to do is wait for your attendees to show up!"),
|
||||
sequence: 210,
|
||||
}, [tour.stepUtils.showAppsMenuItem(), {
|
||||
registry.category("web_tour.tours").add('event_tour', {
|
||||
url: '/odoo',
|
||||
steps: () => [stepUtils.showAppsMenuItem(), {
|
||||
isActive: ["enterprise"],
|
||||
trigger: '.o_app[data-menu-xmlid="event.event_main_menu"]',
|
||||
content: Markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
position: 'bottom',
|
||||
edition: 'enterprise',
|
||||
content: markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
tooltipPosition: 'bottom',
|
||||
run: "click",
|
||||
}, {
|
||||
isActive: ["community"],
|
||||
trigger: '.o_app[data-menu-xmlid="event.event_main_menu"]',
|
||||
content: Markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
edition: 'community',
|
||||
}, {
|
||||
content: markup(_t("Ready to <b>organize events</b> in a few minutes? Let's get started!")),
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: ".o_event_kanban_view",
|
||||
},
|
||||
{
|
||||
trigger: '.o-kanban-button-new',
|
||||
extra_trigger: '.o_event_kanban_view',
|
||||
content: Markup(_t("Let's create your first <b>event</b>.")),
|
||||
position: 'bottom',
|
||||
width: 175,
|
||||
content: markup(_t("Let's create your first <b>event</b>.")),
|
||||
tooltipPosition: 'bottom',
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '.o_event_form_view input[id="name"]',
|
||||
content: Markup(_t("This is the <b>name</b> your guests will see when registering.")),
|
||||
run: 'text Odoo Experience 2020',
|
||||
trigger: '.o_event_form_view div[name="name"] textarea',
|
||||
content: markup(_t("This is the <b>name</b> your guests will see when registering.")),
|
||||
run: "edit Odoo Experience 2020",
|
||||
}, {
|
||||
trigger: '.o_event_form_view div[name="date_end"]',
|
||||
content: _t("Open date range picker. Pick a Start date for your event"),
|
||||
trigger: '.o_event_form_view div[name="date_begin"]',
|
||||
run: function () {
|
||||
$('input[id="date_begin"]').val('09/30/2020 08:00:00').change();
|
||||
$('input[id="date_end"]').val('10/02/2020 23:00:00').change();
|
||||
$('.o_event_form_view input[id="date_end"]').click();
|
||||
const el1 = this.anchor.querySelector('input[data-field="date_begin"]');
|
||||
el1.value = '09/30/2020 08:00:00';
|
||||
el1.dispatchEvent(new Event("change"));
|
||||
const el2 = this.anchor.querySelector('input[data-field="date_end"]');
|
||||
el2.value = '10/02/2020 23:00:00';
|
||||
el2.dispatchEvent(new Event("change"));
|
||||
},
|
||||
}, {
|
||||
content: _t("Apply change."),
|
||||
trigger: '.daterangepicker .applyBtn',
|
||||
in_modal: false,
|
||||
trigger: '.o_event_form_view input[data-field="date_begin"]',
|
||||
content: markup(_t("Open date range picker.<br/>Pick a Start and End date for your event.")),
|
||||
run: "click",
|
||||
}, {
|
||||
trigger: '.o_event_form_view div[name="event_ticket_ids"] .o_field_x2many_list_row_add a',
|
||||
content: Markup(_t("Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one.")),
|
||||
}, tour.stepUtils.autoExpandMoreButtons(),
|
||||
content: markup(_t("Ticket types allow you to distinguish your attendees. Let's <b>create</b> a new one.")),
|
||||
run: "click",
|
||||
}, stepUtils.autoExpandMoreButtons(),
|
||||
...new EventAdditionalTourSteps()._get_website_event_steps(), {
|
||||
trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
content: _t("Now that your event is ready, click here to move it to another stage."),
|
||||
position: 'bottom',
|
||||
}, {
|
||||
tooltipPosition: 'bottom',
|
||||
run: "click",
|
||||
},
|
||||
{
|
||||
trigger: `.o_event_form_view div[name="stage_id"]`,
|
||||
},
|
||||
{
|
||||
trigger: 'ol.breadcrumb li.breadcrumb-item:first',
|
||||
extra_trigger: '.o_event_form_view div[name="stage_id"]',
|
||||
content: Markup(_t("Use the <b>breadcrumbs</b> to go back to your kanban overview.")),
|
||||
position: 'bottom',
|
||||
content: markup(_t("Use the <b>breadcrumbs</b> to go back to your kanban overview.")),
|
||||
tooltipPosition: 'bottom',
|
||||
run: 'click',
|
||||
}].filter(Boolean));
|
||||
|
||||
});
|
||||
}].filter(Boolean)});
|
||||
|
|
|
|||
|
|
@ -1,53 +1,16 @@
|
|||
.o_kanban_view .o_event_kanban_view {
|
||||
.o_kanban_view.o_event_kanban_view .o_kanban_renderer {
|
||||
.o_kanban_record > div {
|
||||
min-height: 140px;
|
||||
}
|
||||
.o_kanban_content {
|
||||
.o_event_fontsize_09 {
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.o_event_fontsize_11 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.o_event_fontsize_20 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_kanban_view .o_event_attendee_kanban_view {
|
||||
@media (max-width: 768px) {
|
||||
.o_event_registration_kanban {
|
||||
min-height: 80px;
|
||||
}
|
||||
.o_kanban_view.o_event_attendee_kanban_view .o_kanban_renderer {
|
||||
.o_kanban_event_registration_event_name{
|
||||
margin-right: 35px;
|
||||
}
|
||||
.oe_kanban_card_ribbon {
|
||||
min-height: 95px;
|
||||
.ribbon {
|
||||
&::before, &::after {
|
||||
display: none;
|
||||
}
|
||||
span {
|
||||
padding: 5px;
|
||||
font-size: small;
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
.ribbon-top-right {
|
||||
margin-top: -1px;
|
||||
span {
|
||||
left: 7px;
|
||||
right: 30px;
|
||||
height: 25px;
|
||||
top: 18px;
|
||||
}
|
||||
}
|
||||
// Used for "Group By"
|
||||
div.row {
|
||||
min-height: 95px;
|
||||
}
|
||||
.o_event_registration_kanban_badge {
|
||||
font-size: 1.2rem;
|
||||
padding: 7px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
/* EVENT BADGE : CARD USED IN ALL FORMATS */
|
||||
|
||||
.o_event_badge_report_page_break {
|
||||
page-break-after:always;
|
||||
}
|
||||
|
||||
.o_event_badge_height {
|
||||
height: 148mm;
|
||||
}
|
||||
|
||||
.o_event_badge_ticket_wrapper {
|
||||
background-color: white;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center center;
|
||||
background-size: cover;
|
||||
border: solid 1px #939393;
|
||||
margin: 16px 8px;
|
||||
padding: 10px 0px 0px;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: -3px 3px 9px 0px rgba(0,0,0,0.51);
|
||||
height: 138mm;
|
||||
|
||||
.o_event_badge_font_faded {
|
||||
color: #939393;
|
||||
}
|
||||
|
||||
.o_event_badge_font_small {
|
||||
@include font-size(.8rem);
|
||||
}
|
||||
|
||||
.o_event_badge_logo {
|
||||
max-height: 84px;
|
||||
max-width: 174px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_badge_ticket_center_vertically {
|
||||
top: 50%;
|
||||
-moz-transform: translateY(-50%);
|
||||
-webkit-transform: translateY(-50%);
|
||||
-ms-transform: translateY(-50%);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
/* EVENT BADGE : A4 FOLDABLE (A4_french_fold) */
|
||||
|
||||
.o_event_foldable_badge_container {
|
||||
|
||||
.o_event_foldable_badge_left_quarter {
|
||||
border-right: .75mm dotted lightgray;
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_bottom_row {
|
||||
border-top: .75mm dotted black;
|
||||
margin-top: .25mm;
|
||||
.o_event_foldable_badge_bottom_left {
|
||||
.o_event_foldable_badge_answer {
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_instructions {
|
||||
background-color: white;
|
||||
.o_event_foldable_badge_step {
|
||||
position: absolute;
|
||||
padding: 3px 5px;
|
||||
margin-top: 5mm;
|
||||
margin-left: 5mm;
|
||||
border-radius: 50%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
.o_event_foldable_badge_container {
|
||||
.o_event_foldable_badge_top {
|
||||
height: 149.4mm;
|
||||
|
||||
&.o_event_foldable_badge_ticket {
|
||||
border-left: 1px dashed black;
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_font_small {
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_bottom.o_event_foldable_badge_left {
|
||||
border-top: 1px dashed black;
|
||||
height: 148mm;
|
||||
|
||||
p {
|
||||
margin: 0px; // to match editor style (.o_field_html p)
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_bottom.o_event_foldable_badge_right {
|
||||
border-left: 1px dashed black;
|
||||
border-top:1px dashed black;
|
||||
height: 148mm;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_ticket {
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(/event/static/src/img/report_foldable_badge_background.png);
|
||||
|
||||
.o_event_foldable_badge_ticket_wrapper {
|
||||
background-color: white;
|
||||
border: solid 1px #939393;
|
||||
border-radius: .7rem;
|
||||
margin: 0px 8px;
|
||||
padding: 10px 3px;
|
||||
font-size: 1.2rem;
|
||||
box-shadow: -3px 3px 9px 0px rgba(0,0,0,0.51);
|
||||
|
||||
.o_event_foldable_badge_ticket_wrapper_top {
|
||||
min-height: 80mm;
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_font_faded {
|
||||
color: #939393;
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_barcode {
|
||||
min-height: 35mm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_foldable_badge_step {
|
||||
position: absolute;
|
||||
padding: 3px 9px;
|
||||
top: 5mm;
|
||||
left: 0mm;
|
||||
border-radius: 50%;
|
||||
background-color: black;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +1,84 @@
|
|||
.o_event_full_page_ticket_footer {
|
||||
.o_event_full_page_ticket_powered_by {
|
||||
font-size: .8rem;
|
||||
@include font-size(.8rem);
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* COMMON FOR FULL PAGE TICKET AND RESPONSIVE HTML VERSION */
|
||||
|
||||
.o_event_full_page_ticket_container {
|
||||
z-index: 1;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(/event/static/src/img/report_full_page_ticket_background.png);
|
||||
padding: 40px 40px 0px 40px;
|
||||
padding-top: 40px;
|
||||
|
||||
.o_event_full_page_ticket_font_faded {
|
||||
color: #939393;
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_small {
|
||||
font-size: .8rem;
|
||||
.o_event_full_page_ticket_details {
|
||||
background-color: white;
|
||||
border: solid 1px #939393;
|
||||
margin: 0px 8px;
|
||||
padding: 10px 3px;
|
||||
box-shadow: -3px 3px 9px 0px rgba(0,0,0,0.51);
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_small_caps {
|
||||
text-transform: uppercase;
|
||||
font-size: .8rem;
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_wrapper {
|
||||
z-index: 2;
|
||||
.o_event_full_page_ticket_details {
|
||||
background-color: white;
|
||||
border: solid 1px #939393;
|
||||
border-radius: .7rem;
|
||||
margin: 0px 8px;
|
||||
padding: 10px 3px;
|
||||
box-shadow: -3px 3px 9px 0px rgba(0,0,0,0.51);
|
||||
|
||||
.o_event_full_page_ticket_event_logo {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_barcode {
|
||||
padding-left: 30px;
|
||||
|
||||
.o_event_full_page_ticket_barcode_container {
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
width: 67px;
|
||||
|
||||
img {
|
||||
padding: 0px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.o_event_full_page_ticket_barcode {
|
||||
min-height: 344px;
|
||||
&.o_event_full_page_ticket_qr_only {
|
||||
min-height: 168px;
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_answer {
|
||||
font-size: .875rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* FULL PAGE TICKET - PDF REPORT */
|
||||
|
||||
.o_event_full_page_ticket {
|
||||
|
||||
.o_event_full_page_left_details {
|
||||
width: 78%;
|
||||
.o_event_full_page_left_details_top {
|
||||
// Give at least height of qr code to allow bottom row to take 100%
|
||||
// Without overlapping the qr code
|
||||
min-height: 152px;
|
||||
}
|
||||
.o_event_full_page_left_details_bottom_qr_only {
|
||||
// parent div only has 78% -> 128% of 78% is just below 100%
|
||||
width: 128%;
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_barcode {
|
||||
width: 22%;
|
||||
}
|
||||
.o_event_full_page_ticket_column {
|
||||
width: 50%;
|
||||
// Prevent text overfow on the next column. While columns have the expected
|
||||
// width, the text will overflow in address and date, most probably due to
|
||||
// the fa-icons. This prevents this overflow and makes sure we see the full
|
||||
// content of address and date.
|
||||
padding-right: 45px;
|
||||
}
|
||||
.o_event_full_page_ticket_answer {
|
||||
border-radius: 6px;
|
||||
}
|
||||
.o_event_full_page_extra_instructions {
|
||||
width: 100%;
|
||||
p {
|
||||
margin: 0px; // to match editor style (.o_field_html p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_full_page_ticket_container .o_event_full_page_ticket_barcode {
|
||||
.o_event_barcode {
|
||||
// Barcode image has fixed size. These values are hard-coded to
|
||||
// have it aligned correctly in the pdf report.
|
||||
margin: 28px 0 0 -8px;
|
||||
-webkit-transform: rotate(90deg);
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
/* FULL PAGE TICKET - RESONSIVE HTML WEB VERSION */
|
||||
|
||||
.o_event_full_page_ticket_responsive_html {
|
||||
.o_event_full_page_ticket_details {
|
||||
max-width: 1000px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
.o_event_full_page_left_details {
|
||||
width: 100%;
|
||||
}
|
||||
.o_event_full_page_ticket_barcode {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.o_event_download_ticket_btn:hover {
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.o_event_full_page_left_details {
|
||||
width: calc(100% - 168px);
|
||||
}
|
||||
.o_event_full_page_ticket_barcode {
|
||||
width: 168px;
|
||||
}
|
||||
// Give details more space for small screens
|
||||
@media (max-width: map-get($grid-breakpoints, "md")) {
|
||||
.o_event_full_page_left_details {
|
||||
width: calc(100% - 152px);
|
||||
padding-left: 0.5rem !important;
|
||||
}
|
||||
.o_event_full_page_ticket_barcode {
|
||||
width: 152px;
|
||||
.o_event_full_page_ticket_barcode_container {
|
||||
padding-left: 0px !important;
|
||||
padding-right: 0px !important;
|
||||
}
|
||||
}
|
||||
.o_event_full_page_ticket_type {
|
||||
padding-right: 0rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { _t } from "@web/core/l10n/translation";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { ReferenceField, referenceField } from "@web/views/fields/reference/reference_field";
|
||||
|
||||
export class EventMailTemplateReferenceField extends ReferenceField {
|
||||
static template = "event.mailTemplateReferenceField";
|
||||
|
||||
setup() {
|
||||
const returnVal = super.setup();
|
||||
// select the first value by default
|
||||
// selection is [['mail', 'Mail Template'], ['sms', 'Sms Template'], ...]
|
||||
const defaultSelect = this.selection?.[0][0];
|
||||
if (defaultSelect) {
|
||||
this.state.currentRelation = defaultSelect;
|
||||
}
|
||||
return returnVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make not openable in readonly mode
|
||||
* as we expect m2o fields to just be strings in list view
|
||||
*/
|
||||
get m2oProps() {
|
||||
const props = super.m2oProps;
|
||||
if (props.readonly) {
|
||||
props.canOpen = false;
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
export const eventMailTemplateReferenceField = {
|
||||
...referenceField,
|
||||
component: EventMailTemplateReferenceField,
|
||||
displayName: _t("Event Mail Template Reference"),
|
||||
};
|
||||
|
||||
registry.category("fields").add("EventMailTemplateReferenceField", eventMailTemplateReferenceField);
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.o_list_renderer .o_field_EventMailTemplateReferenceField div.o_mail_template_reference_field_icon {
|
||||
margin-right: 0.5rem;
|
||||
max-width: 1.5rem !important;
|
||||
width: 1.5rem !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="event.mailTemplateReferenceField" t-inherit="web.ReferenceField" t-inherit-mode="primary">
|
||||
<xpath expr="//t[@t-if='!props.readonly and !hideModelSelector']" position="after">
|
||||
<t t-elif="getValue()">
|
||||
<div class="o_mail_template_reference_field_icon d-flex justify-content-center flex-grow-0 flex-shrink-0">
|
||||
<span class="event_template_reference_mail fa fa-envelope" t-if="relation === 'mail.template'"/>
|
||||
</div>
|
||||
</t>
|
||||
</xpath>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { kanbanView } from "@web/views/kanban/kanban_view";
|
||||
import { KanbanController } from "@web/views/kanban/kanban_controller";
|
||||
import { EventRegistrationSummaryDialog } from "@event/client_action/event_registration_summary_dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
|
||||
export class EventRegistrationKanbanController extends KanbanController {
|
||||
|
||||
setup() {
|
||||
super.setup()
|
||||
this.dialog = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
async openRecord(record) {
|
||||
if (this.props.context.is_registration_desk_view) {
|
||||
const barcode = record.data.barcode;
|
||||
const eventId = record.data.event_id.id;
|
||||
|
||||
const result = await this.orm.call("event.registration", "register_attendee", [], {
|
||||
barcode: barcode,
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
this.dialog.add(
|
||||
EventRegistrationSummaryDialog,
|
||||
{
|
||||
model: this.model,
|
||||
registration: result
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return super.openRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EventRegistrationKanbanView = {
|
||||
...kanbanView,
|
||||
Controller: EventRegistrationKanbanController,
|
||||
}
|
||||
|
||||
registry.category("views").add("registration_summary_dialog_kanban", EventRegistrationKanbanView);
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import { EventRegistrationSummaryDialog } from "@event/client_action/event_registration_summary_dialog";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { listView } from "@web/views/list/list_view";
|
||||
import { ListController } from "@web/views/list/list_controller";
|
||||
|
||||
export class EventRegistrationListController extends ListController {
|
||||
|
||||
setup() {
|
||||
super.setup();
|
||||
this.dialog = useService("dialog");
|
||||
this.orm = useService("orm");
|
||||
}
|
||||
|
||||
async openRecord(record) {
|
||||
if (this.props.context.is_registration_desk_view) {
|
||||
const barcode = record.data.barcode;
|
||||
const eventId = record.data.event_id.id;
|
||||
|
||||
const result = await this.orm.call("event.registration", "register_attendee", [], {
|
||||
barcode: barcode,
|
||||
event_id: eventId,
|
||||
});
|
||||
|
||||
this.dialog.add(
|
||||
EventRegistrationSummaryDialog,
|
||||
{
|
||||
model: this.model,
|
||||
registration: result
|
||||
}
|
||||
);
|
||||
} else {
|
||||
return super.openRecord(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const EventRegistrationListView = {
|
||||
...listView,
|
||||
Controller: EventRegistrationListController,
|
||||
}
|
||||
|
||||
registry.category("views").add("registration_summary_dialog_list", EventRegistrationListView);
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="event.EventSlotCalendarCommonRenderer.event">
|
||||
<!-- Display end time and hide title on the full calendar library event. -->
|
||||
<span t-if="!isTimeHidden" class="fc-time">
|
||||
<t t-out="startTime"/> - <t t-out="endTime"/>
|
||||
</span>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { CalendarController } from "@web/views/calendar/calendar_controller";
|
||||
import { _t } from "@web/core/l10n/translation";
|
||||
|
||||
export class EventSlotCalendarController extends CalendarController {
|
||||
/**
|
||||
* Rename mobile quick create dialog.
|
||||
* Load model after save to show created record.
|
||||
*/
|
||||
getQuickCreateFormViewProps(record) {
|
||||
return {
|
||||
...super.getQuickCreateFormViewProps(record),
|
||||
onRecordSaved: () => this.model.load(),
|
||||
title: _t("New Slot"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { CalendarModel } from "@web/views/calendar/calendar_model";
|
||||
import { serializeDate } from "@web/core/l10n/dates";
|
||||
|
||||
/**
|
||||
* Slots are created differently depending on the screen size.
|
||||
* Desktop: Using the multi create feature.
|
||||
* Mobile: Using the calendar quick create dialog.
|
||||
*/
|
||||
export class EventSlotCalendarModel extends CalendarModel {
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Set slot date and hours from selected datetimes.
|
||||
*/
|
||||
buildRawRecord(partialRecord, options = {}) {
|
||||
const rawRecord = super.buildRawRecord(partialRecord, options);
|
||||
rawRecord["date"] = serializeDate(partialRecord.start);
|
||||
rawRecord["start_hour"] = partialRecord.start.hour + partialRecord.start.minute / 60;
|
||||
// There could be no 'end' when opening the mobile quick create dialog.
|
||||
if (partialRecord.end) {
|
||||
rawRecord["end_hour"] = partialRecord.end.hour + partialRecord.end.minute / 60;
|
||||
}
|
||||
return rawRecord;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Save selected date in context defaults for mobile quick create form dialog.
|
||||
*
|
||||
* Only needed for mobile quick create as the desktop multi create feature
|
||||
* is directly saving the raw records with the 'date' returned from 'buildRawRecord'.
|
||||
*/
|
||||
makeContextDefaults(rawRecord) {
|
||||
const context = super.makeContextDefaults(rawRecord);
|
||||
context["default_date"] = rawRecord["date"];
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Instead of the local tz, express the times in the related event tz or fallback on utc.
|
||||
*
|
||||
* After conversion to event tz, changing the 'zone' param back to local without changing
|
||||
* the already converted datetimes. This is to make sure the calendar renders records correctly
|
||||
* as it always expects datetimes expressed in the 'local' tz.
|
||||
*/
|
||||
normalizeRecord(rawRecord) {
|
||||
const normalizedRecord = super.normalizeRecord(rawRecord);
|
||||
const tz = rawRecord.date_tz || 'utc';
|
||||
normalizedRecord.start = normalizedRecord.start.setZone(tz).setZone('local', {keepLocalTime: true});
|
||||
normalizedRecord.end = normalizedRecord.end.setZone(tz).setZone('local', {keepLocalTime: true});
|
||||
// Always display the slot time
|
||||
normalizedRecord.isTimeHidden = false;
|
||||
return normalizedRecord;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
import { CalendarCommonRenderer } from "@web/views/calendar/calendar_common/calendar_common_renderer";
|
||||
import { CalendarRenderer } from "@web/views/calendar/calendar_renderer";
|
||||
import { CalendarYearRenderer } from "@web/views/calendar/calendar_year/calendar_year_renderer";
|
||||
|
||||
export class EventSlotCalendarCommonRenderer extends CalendarCommonRenderer {
|
||||
// Display end time and hide title on the full calendar library event.
|
||||
static eventTemplate = "event.EventSlotCalendarCommonRenderer.event";
|
||||
// Prevent square selection over disabled cells on desktop.
|
||||
static cellIsSelectable = (cell) => !cell.classList.contains("o_calendar_disabled");
|
||||
|
||||
setup() {
|
||||
super.setup(...arguments);
|
||||
this.rangeStartDate = this.props.model.meta.context.event_calendar_range_start_date;
|
||||
this.rangeEndDate = this.props.model.meta.context.event_calendar_range_end_date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add overlay to disable days outside of event time range.
|
||||
*/
|
||||
getDayCellClassNames(info) {
|
||||
const date = luxon.DateTime.fromJSDate(info.date).toISODate();
|
||||
if (
|
||||
this.rangeStartDate &&
|
||||
this.rangeEndDate &&
|
||||
(date < this.rangeStartDate || date > this.rangeEndDate)
|
||||
) {
|
||||
return ["o_calendar_disabled"];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Slots cannot be created over multiple days on mobile.
|
||||
* On desktop, using the multi create feature which doesn't consider this value.
|
||||
*/
|
||||
isSelectionAllowed(event) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* Prevent click on disabled dates in mobile.
|
||||
*/
|
||||
onDateClick(info) {
|
||||
if (info.dayEl.classList.contains("o_calendar_disabled")) {
|
||||
return;
|
||||
}
|
||||
return super.onDateClick(info);
|
||||
}
|
||||
}
|
||||
|
||||
export class EventSlotCalendarRenderer extends CalendarRenderer {
|
||||
static components = {
|
||||
...CalendarRenderer.components,
|
||||
day: EventSlotCalendarCommonRenderer,
|
||||
week: EventSlotCalendarCommonRenderer,
|
||||
month: EventSlotCalendarCommonRenderer,
|
||||
year: CalendarYearRenderer,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { calendarView } from "@web/views/calendar/calendar_view";
|
||||
import { EventSlotCalendarController } from "@event/views/event_slot_calendar/event_slot_calendar_controller";
|
||||
import { EventSlotCalendarModel } from "@event/views/event_slot_calendar/event_slot_calendar_model";
|
||||
import { EventSlotCalendarRenderer } from "@event/views/event_slot_calendar/event_slot_calendar_renderer";
|
||||
import { registry } from "@web/core/registry";
|
||||
|
||||
|
||||
export const EventSlotCalendarView = {
|
||||
...calendarView,
|
||||
Controller: EventSlotCalendarController,
|
||||
Model: EventSlotCalendarModel,
|
||||
Renderer: EventSlotCalendarRenderer,
|
||||
};
|
||||
|
||||
registry.category("views").add("event_slot_calendar", EventSlotCalendarView);
|
||||