Skip to content

Commit 2f4d503

Browse files
committed
- Added the obsidian RegistrationTemplateDetail block and chopped the legacy webforms version.
1 parent c63bea1 commit 2f4d503

43 files changed

Lines changed: 10372 additions & 5412 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Rock.Blocks/Event/RegistrationTemplateDetail.cs‎

Lines changed: 3381 additions & 0 deletions
Large diffs are not rendered by default.

Rock/Model/Event/RegistrationTemplate/RegistrationNotify.cs renamed to Rock.Enums/Event/RegistrationNotify.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// See the License for the specific language governing permissions and
1414
// limitations under the License.
1515
// </copyright>
16+
//
1617

1718
using System;
1819

@@ -22,6 +23,7 @@ namespace Rock.Model
2223
/// Flag indicating who should be notified on a new registration
2324
/// </summary>
2425
[Flags]
26+
[Enums.EnumDomain( "Event" )]
2527
public enum RegistrationNotify
2628
{
2729
/// <summary>
Lines changed: 265 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,265 @@
1+
<!-- Copyright by the Spark Development Network; Licensed under the Rock Community License -->
2+
<template>
3+
<Grid :data="gridData" keyField="guid" itemTerm="Discount" liveUpdates light @addItem="onAddDiscount">
4+
<ReorderColumn @orderChanged="onOrderChanged" />
5+
<TextColumn name="code" field="code" title="Code" />
6+
<TextColumn name="discount" field="discount" title="Discount" />
7+
<TextColumn name="limits" field="limits" title="Limits" />
8+
<EditColumn @click="onEditDiscount" />
9+
<DeleteColumn @click="onDeleteDiscount" />
10+
</Grid>
11+
12+
<Modal v-model="isModalVisible" title="Discount Code" saveText="Save" cancelText="Cancel" :onSave="onSaveDiscount">
13+
<NotificationBox v-if="modalErrorMessage" alertType="warning">{{ modalErrorMessage }}</NotificationBox>
14+
15+
<div class="row">
16+
<div class="col-md-6">
17+
<TextBox v-model="editCode" label="Discount Code" rules="required" :maxLength="100" />
18+
19+
<RadioButtonList v-model="editDiscountType" label="Discount Type" horizontal :items="discountTypeItems" />
20+
21+
<NumberBox v-if="isPercentage" v-model="editPercentage" label="Discount Percentage" :decimalCount="0" :minimumValue="0" :maximumValue="100" inputClasses="input-width-md">
22+
<template #inputGroupAppend>
23+
<span class="input-group-addon">%</span>
24+
</template>
25+
</NumberBox>
26+
27+
<CurrencyBox v-else v-model="editAmount" label="Discount Amount" />
28+
29+
<CheckBox v-model="editAutoApplyDiscount"
30+
label="Auto Apply Discount"
31+
help="Will automatically apply the discount if the registration meets the criteria. If multiple automatic discounts exist, only the first one that meets the criteria will be applied." />
32+
</div>
33+
34+
<div class="col-md-6">
35+
<NumberBox v-model="editMaxUsage"
36+
label="Maximum Usage"
37+
:decimalCount="0"
38+
:minimumValue="0"
39+
help="The maximum number of registrations that the discount code can be used on (leave blank for none)." />
40+
41+
<NumberBox v-model="editMaxRegistrants"
42+
label="Maximum Registrants"
43+
:decimalCount="0"
44+
:minimumValue="0"
45+
help="The maximum number of registrants in a single registration that the discount code can be used on." />
46+
47+
<NumberBox v-model="editMinRegistrants"
48+
label="Minimum Registrants"
49+
:decimalCount="0"
50+
:minimumValue="0"
51+
help="The minimum number of registrants in the registration that are required in order to use the discount code." />
52+
53+
<DateRangePicker v-model="editDateRange" label="Effective Dates" help="The beginning and/or ending date that the discount code can be used." />
54+
</div>
55+
</div>
56+
</Modal>
57+
</template>
58+
59+
<script setup lang="ts">
60+
import { computed, PropType, ref } from "vue";
61+
import Grid, { DeleteColumn, EditColumn, ReorderColumn, TextColumn } from "@Obsidian/Controls/grid";
62+
import CheckBox from "@Obsidian/Controls/checkBox.obs";
63+
import CurrencyBox from "@Obsidian/Controls/currencyBox.obs";
64+
import DateRangePicker from "@Obsidian/Controls/dateRangePicker.obs";
65+
import Modal from "@Obsidian/Controls/modal.obs";
66+
import NotificationBox from "@Obsidian/Controls/notificationBox.obs";
67+
import NumberBox from "@Obsidian/Controls/numberBox.obs";
68+
import RadioButtonList from "@Obsidian/Controls/radioButtonList.obs";
69+
import TextBox from "@Obsidian/Controls/textBox.obs";
70+
import { DateRangeParts } from "@Obsidian/Types/Controls/dateRangePicker";
71+
import { newGuid } from "@Obsidian/Utility/guid";
72+
import { CurrencyInfoBag } from "@Obsidian/ViewModels/Utility/currencyInfoBag";
73+
import { ListItemBag } from "@Obsidian/ViewModels/Utility/listItemBag";
74+
import { RegistrationTemplateDiscountBag } from "@Obsidian/ViewModels/Blocks/Event/RegistrationTemplateDetail/registrationTemplateDiscountBag";
75+
import { DiscountType } from "./types.partial";
76+
import { formatDiscount, formatDiscountLimits, getNextOrder, moveItem, renumberOrder } from "./utils.partial";
77+
78+
const props = defineProps({
79+
/** The discount codes of the template. */
80+
discounts: {
81+
type: Array as PropType<RegistrationTemplateDiscountBag[]>,
82+
required: true
83+
},
84+
85+
/** The currency settings of the organization. */
86+
currencyInfo: {
87+
type: Object as PropType<CurrencyInfoBag | null>,
88+
required: false
89+
}
90+
});
91+
92+
const emit = defineEmits<{
93+
(e: "update:discounts", value: RegistrationTemplateDiscountBag[]): void
94+
}>();
95+
96+
// #region Values
97+
98+
const isModalVisible = ref(false);
99+
const modalErrorMessage = ref("");
100+
101+
/** The unique identifier of the discount being edited, or null when adding. */
102+
const editingGuid = ref<string | null>(null);
103+
const editCode = ref("");
104+
const editDiscountType = ref<string>(DiscountType.Percentage);
105+
const editPercentage = ref<number | null>(null);
106+
const editAmount = ref<number | null>(null);
107+
const editAutoApplyDiscount = ref(false);
108+
const editMaxUsage = ref<number | null>(null);
109+
const editMaxRegistrants = ref<number | null>(null);
110+
const editMinRegistrants = ref<number | null>(null);
111+
const editDateRange = ref<DateRangeParts | undefined>(undefined);
112+
113+
const discountTypeItems: ListItemBag[] = [
114+
{ value: DiscountType.Percentage, text: "Percentage" },
115+
{ value: DiscountType.Amount, text: "Amount" }
116+
];
117+
118+
// #endregion Values
119+
120+
// #region Computed Values
121+
122+
const discounts = computed({
123+
get(): RegistrationTemplateDiscountBag[] {
124+
return props.discounts ?? [];
125+
},
126+
set(value: RegistrationTemplateDiscountBag[]) {
127+
emit("update:discounts", value);
128+
}
129+
});
130+
131+
const gridData = computed(() => {
132+
const rows = [...discounts.value]
133+
.sort((a, b) => a.order - b.order)
134+
.map(discount => ({
135+
guid: discount.guid,
136+
code: discount.code ?? "",
137+
discount: formatDiscount(discount, props.currencyInfo),
138+
limits: formatDiscountLimits(discount)
139+
}));
140+
141+
return { rows };
142+
});
143+
144+
const isPercentage = computed((): boolean => {
145+
return editDiscountType.value !== DiscountType.Amount;
146+
});
147+
148+
// #endregion Computed Values
149+
150+
// #region Event Handlers
151+
152+
/**
153+
* Called when the Add button on the grid is clicked.
154+
*/
155+
function onAddDiscount(): void {
156+
openModal(null);
157+
}
158+
159+
/**
160+
* Called when the Edit button for a row is clicked.
161+
*
162+
* @param key The unique identifier of the discount to edit.
163+
*/
164+
function onEditDiscount(key: string): void {
165+
const discount = discounts.value.find(d => d.guid === key);
166+
167+
if (discount) {
168+
openModal(discount);
169+
}
170+
}
171+
172+
/**
173+
* Called when the Delete button for a row is clicked.
174+
*
175+
* @param key The unique identifier of the discount to delete.
176+
*/
177+
function onDeleteDiscount(key: string): void {
178+
discounts.value = renumberOrder(discounts.value.filter(d => d.guid !== key));
179+
}
180+
181+
/**
182+
* Called when a row is reordered in the grid.
183+
*
184+
* @param item The row being moved.
185+
* @param beforeItem The row the moved item was placed before, or null if moved to the end.
186+
*
187+
* @returns True to keep the new order.
188+
*/
189+
async function onOrderChanged(item: Record<string, unknown>, beforeItem: Record<string, unknown> | null): Promise<boolean> {
190+
discounts.value = moveItem([...discounts.value].sort((a, b) => a.order - b.order), item["guid"] as string, (beforeItem?.["guid"] as string | undefined) ?? null, d => d.guid);
191+
192+
return true;
193+
}
194+
195+
/**
196+
* Called when the modal Save button is clicked. Validates the discount code
197+
* is unique and then replaces or appends the discount.
198+
*/
199+
function onSaveDiscount(): void {
200+
modalErrorMessage.value = "";
201+
202+
const code = editCode.value.trim();
203+
const isDuplicateCode = discounts.value.some(d => d.guid !== editingGuid.value && (d.code ?? "").trim().toLowerCase() === code.toLowerCase());
204+
205+
if (isDuplicateCode) {
206+
modalErrorMessage.value = `The discount code "${code}" is already in use.`;
207+
208+
return;
209+
}
210+
211+
const existing = editingGuid.value ? discounts.value.find(d => d.guid === editingGuid.value) : undefined;
212+
213+
const discount: RegistrationTemplateDiscountBag = {
214+
guid: existing?.guid ?? newGuid(),
215+
order: existing?.order ?? getNextOrder(discounts.value),
216+
code: code,
217+
discountPercentage: isPercentage.value ? (editPercentage.value ?? 0) * 0.01 : 0,
218+
discountAmount: isPercentage.value ? 0 : (editAmount.value ?? 0),
219+
maxUsage: editMaxUsage.value,
220+
maxRegistrants: editMaxRegistrants.value,
221+
minRegistrants: editMinRegistrants.value,
222+
startDate: editDateRange.value?.lowerValue || null,
223+
endDate: editDateRange.value?.upperValue || null,
224+
autoApplyDiscount: editAutoApplyDiscount.value
225+
};
226+
227+
if (existing) {
228+
discounts.value = discounts.value.map(d => d.guid === existing.guid ? discount : d);
229+
}
230+
else {
231+
discounts.value = [...discounts.value, discount];
232+
}
233+
234+
isModalVisible.value = false;
235+
}
236+
237+
// #endregion Event Handlers
238+
239+
// #region Functions
240+
241+
/**
242+
* Opens the modal for a new discount, or populates it with an existing discount.
243+
*
244+
* @param discount The discount to edit, or null to add a new discount.
245+
*/
246+
function openModal(discount: RegistrationTemplateDiscountBag | null): void {
247+
modalErrorMessage.value = "";
248+
editingGuid.value = discount?.guid ?? null;
249+
editCode.value = discount?.code ?? "";
250+
editDiscountType.value = (discount?.discountAmount ?? 0) > 0 ? DiscountType.Amount : DiscountType.Percentage;
251+
editPercentage.value = discount ? Math.round(discount.discountPercentage * 100) : null;
252+
editAmount.value = discount && discount.discountAmount > 0 ? discount.discountAmount : null;
253+
editAutoApplyDiscount.value = discount?.autoApplyDiscount ?? false;
254+
editMaxUsage.value = discount?.maxUsage ?? null;
255+
editMaxRegistrants.value = discount?.maxRegistrants ?? null;
256+
editMinRegistrants.value = discount?.minRegistrants ?? null;
257+
editDateRange.value = discount?.startDate || discount?.endDate
258+
? { lowerValue: discount.startDate ?? "", upperValue: discount.endDate ?? "" }
259+
: undefined;
260+
261+
isModalVisible.value = true;
262+
}
263+
264+
// #endregion Functions
265+
</script>

0 commit comments

Comments
 (0)