@osimart/create-storefront 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div v-if="fees.length" class="flex flex-col gap-3">
|
|
3
|
+
<div
|
|
4
|
+
class="divide-y divide-gray-200 overflow-hidden rounded-xl border border-gray-300 bg-white"
|
|
5
|
+
>
|
|
6
|
+
<template v-for="fee in fees" :key="fee.id">
|
|
7
|
+
<label
|
|
8
|
+
class="flex items-center gap-3 px-3 py-2.5"
|
|
9
|
+
:class="[
|
|
10
|
+
isChecked(fee) ? 'bg-primaryColor bg-opacity-5' : '',
|
|
11
|
+
fee.is_required ? 'cursor-default' : 'cursor-pointer'
|
|
12
|
+
]"
|
|
13
|
+
>
|
|
14
|
+
<input
|
|
15
|
+
type="checkbox"
|
|
16
|
+
:checked="isChecked(fee)"
|
|
17
|
+
:disabled="fee.is_required"
|
|
18
|
+
class="h-4 w-4 flex-shrink-0 cursor-pointer appearance-none rounded-full border-2 border-gray-300 transition-colors checked:border-primaryColor checked:bg-primaryColor checked:ring-2 checked:ring-white disabled:opacity-50"
|
|
19
|
+
@change="toggleFee(fee)"
|
|
20
|
+
/>
|
|
21
|
+
<img v-if="fee.logo" :src="mediaUrl(fee.logo)" width="22" height="22" :alt="fee.beneficiary || fee.name" />
|
|
22
|
+
<span class="flex-1 text-sm">
|
|
23
|
+
{{ fee.name }}
|
|
24
|
+
<span v-if="fee.is_required" class="ml-1 text-xs font-normal text-gray-500">(included)</span>
|
|
25
|
+
<span v-if="fee.description" class="mt-0.5 block text-xs text-gray-500">{{ fee.description }}</span>
|
|
26
|
+
<span v-if="fee.beneficiary" class="mt-0.5 block text-xs text-gray-400">{{ fee.beneficiary }}</span>
|
|
27
|
+
</span>
|
|
28
|
+
<span v-if="!isCustomerAmount(fee)" class="flex-shrink-0 text-sm font-medium text-primaryColor">
|
|
29
|
+
{{ label(fee) }}
|
|
30
|
+
</span>
|
|
31
|
+
</label>
|
|
32
|
+
|
|
33
|
+
<!-- Amount Form (Revealed when checked & fee is Customer Amount) -->
|
|
34
|
+
<div
|
|
35
|
+
v-if="isChecked(fee) && isCustomerAmount(fee)"
|
|
36
|
+
class="space-y-3 bg-gray-50/50 p-4"
|
|
37
|
+
>
|
|
38
|
+
<span class="text-xs font-semibold text-gray-600 block">Select Donation Amount:</span>
|
|
39
|
+
|
|
40
|
+
<!-- Span Pills -->
|
|
41
|
+
<div class="flex flex-wrap gap-2">
|
|
42
|
+
<span
|
|
43
|
+
v-for="preset in getPresets(fee)"
|
|
44
|
+
:key="preset"
|
|
45
|
+
@click="selectPreset(fee, preset)"
|
|
46
|
+
class="px-3 py-1.5 text-xs font-medium rounded-full border cursor-pointer transition-all select-none"
|
|
47
|
+
:class="selectedMode[fee.id] === 'preset' && Number(selection[fee.id]) === preset
|
|
48
|
+
? 'bg-primaryColor text-white border-primaryColor shadow-sm'
|
|
49
|
+
: 'bg-white text-gray-700 border-gray-300 hover:border-primaryColor hover:text-primaryColor'"
|
|
50
|
+
>
|
|
51
|
+
{{ currency }}{{ preset }}
|
|
52
|
+
</span>
|
|
53
|
+
<span
|
|
54
|
+
@click="selectOther(fee)"
|
|
55
|
+
class="px-3 py-1.5 text-xs font-medium rounded-full border cursor-pointer transition-all select-none"
|
|
56
|
+
:class="selectedMode[fee.id] === 'other'
|
|
57
|
+
? 'bg-primaryColor text-white border-primaryColor shadow-sm'
|
|
58
|
+
: 'bg-white text-gray-700 border-gray-300 hover:border-primaryColor hover:text-primaryColor'"
|
|
59
|
+
>
|
|
60
|
+
Other
|
|
61
|
+
</span>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Custom Input Field (ONLY displayed if 'Other' is selected) -->
|
|
65
|
+
<div v-if="selectedMode[fee.id] === 'other'" class="pt-1">
|
|
66
|
+
<div class="relative flex items-center">
|
|
67
|
+
<span class="osi-fee-currency" aria-hidden="true">{{ currency }}</span>
|
|
68
|
+
<input
|
|
69
|
+
type="number"
|
|
70
|
+
:min="fee.min_amount || 0"
|
|
71
|
+
:max="fee.max_amount || undefined"
|
|
72
|
+
step="0.5"
|
|
73
|
+
:value="selection[fee.id]"
|
|
74
|
+
@input="setAmount(fee, $event.target.value)"
|
|
75
|
+
@blur="clamp(fee)"
|
|
76
|
+
class="osi-input osi-fee-amount"
|
|
77
|
+
aria-label="Custom amount"
|
|
78
|
+
placeholder="Enter custom amount"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<!-- Error message if validation fails -->
|
|
84
|
+
<p v-if="errors[fee.id]" class="text-xs text-red-600 font-medium">
|
|
85
|
+
{{ errors[fee.id] }}
|
|
86
|
+
</p>
|
|
87
|
+
</div>
|
|
88
|
+
</template>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<script>
|
|
94
|
+
export default {
|
|
95
|
+
name: "ExtraFees",
|
|
96
|
+
setup() {
|
|
97
|
+
// Tenant-resolved media host: one Nuxt process serves every store, so the
|
|
98
|
+
// logo host has to come from useMediaUrl(), not a build-time constant.
|
|
99
|
+
const { mediaUrl } = useMediaUrl();
|
|
100
|
+
const { store } = useStorefront();
|
|
101
|
+
const currency = computed(
|
|
102
|
+
() => store.value?.default_currency?.symbol || store.value?.default_currency?.code || "$"
|
|
103
|
+
);
|
|
104
|
+
return { mediaUrl, currency };
|
|
105
|
+
},
|
|
106
|
+
props: {
|
|
107
|
+
fees: { type: Array, default: () => [] },
|
|
108
|
+
subtotal: { type: Number, default: 0 },
|
|
109
|
+
},
|
|
110
|
+
emits: ["change"],
|
|
111
|
+
data() {
|
|
112
|
+
return {
|
|
113
|
+
selection: {},
|
|
114
|
+
selectedMode: {}, // { [feeId]: 'preset' | 'other' }
|
|
115
|
+
errors: {},
|
|
116
|
+
};
|
|
117
|
+
},
|
|
118
|
+
methods: {
|
|
119
|
+
isCustomerAmount(fee) {
|
|
120
|
+
if (!fee) return true;
|
|
121
|
+
const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase().replace(/_/g, " ");
|
|
122
|
+
if (calc.includes("percentage") || calc.includes("fixed")) return false;
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
isChecked(fee) {
|
|
127
|
+
return Object.prototype.hasOwnProperty.call(this.selection, fee.id);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
toggleFee(fee) {
|
|
131
|
+
if (fee.is_required) return;
|
|
132
|
+
if (this.isChecked(fee)) {
|
|
133
|
+
const newSelection = { ...this.selection };
|
|
134
|
+
delete newSelection[fee.id];
|
|
135
|
+
this.selection = newSelection;
|
|
136
|
+
|
|
137
|
+
const newMode = { ...this.selectedMode };
|
|
138
|
+
delete newMode[fee.id];
|
|
139
|
+
this.selectedMode = newMode;
|
|
140
|
+
|
|
141
|
+
const newErrors = { ...this.errors };
|
|
142
|
+
delete newErrors[fee.id];
|
|
143
|
+
this.errors = newErrors;
|
|
144
|
+
} else {
|
|
145
|
+
if (this.isCustomerAmount(fee)) {
|
|
146
|
+
const presets = this.getPresets(fee);
|
|
147
|
+
const defaultPreset = presets.length ? presets[0] : (Number(fee.min_amount) || 5);
|
|
148
|
+
this.selection = { ...this.selection, [fee.id]: defaultPreset };
|
|
149
|
+
this.selectedMode = { ...this.selectedMode, [fee.id]: "preset" };
|
|
150
|
+
} else {
|
|
151
|
+
this.selection = { ...this.selection, [fee.id]: null };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
this.emitChange();
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
getPresets(fee) {
|
|
158
|
+
const min = Number(fee.min_amount) || 1;
|
|
159
|
+
const max = Number(fee.max_amount) || 1000;
|
|
160
|
+
const candidates = [1,5,10,25,50];
|
|
161
|
+
const presets = candidates.filter(v => v >= min && v <= max);
|
|
162
|
+
if (presets.length < 3) {
|
|
163
|
+
const generated = [];
|
|
164
|
+
let val = Math.max(min, 1);
|
|
165
|
+
while (generated.length < 4 && val <= max) {
|
|
166
|
+
generated.push(val);
|
|
167
|
+
val = val * 2;
|
|
168
|
+
}
|
|
169
|
+
return generated;
|
|
170
|
+
}
|
|
171
|
+
return presets;
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
selectPreset(fee, preset) {
|
|
175
|
+
this.selectedMode = { ...this.selectedMode, [fee.id]: "preset" };
|
|
176
|
+
this.selection = { ...this.selection, [fee.id]: preset };
|
|
177
|
+
const newErrors = { ...this.errors };
|
|
178
|
+
delete newErrors[fee.id];
|
|
179
|
+
this.errors = newErrors;
|
|
180
|
+
this.emitChange();
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
selectOther(fee) {
|
|
184
|
+
this.selectedMode = { ...this.selectedMode, [fee.id]: "other" };
|
|
185
|
+
this.selection = { ...this.selection, [fee.id]: null };
|
|
186
|
+
this.validate(fee);
|
|
187
|
+
this.emitChange();
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
setAmount(fee, raw) {
|
|
191
|
+
const val = raw === "" ? null : Number(raw);
|
|
192
|
+
this.selection = { ...this.selection, [fee.id]: val };
|
|
193
|
+
this.validate(fee);
|
|
194
|
+
this.emitChange();
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
clamp(fee) {
|
|
198
|
+
const raw = this.selection[fee.id];
|
|
199
|
+
|
|
200
|
+
// Leave it empty — don't force a default value in
|
|
201
|
+
if (raw === null || raw === "" || raw === undefined) {
|
|
202
|
+
this.validate(fee);
|
|
203
|
+
this.emitChange();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let v = Number(raw);
|
|
208
|
+
if (fee.min_amount != null) v = Math.max(v, Number(fee.min_amount));
|
|
209
|
+
if (fee.max_amount != null) v = Math.min(v, Number(fee.max_amount));
|
|
210
|
+
this.selection = { ...this.selection, [fee.id]: v };
|
|
211
|
+
this.validate(fee);
|
|
212
|
+
this.emitChange();
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
validate(fee) {
|
|
216
|
+
const v = Number(this.selection[fee.id]);
|
|
217
|
+
if (!this.isCustomerAmount(fee)) return true;
|
|
218
|
+
|
|
219
|
+
const raw = this.selection[fee.id];
|
|
220
|
+
const isEmpty = raw === null || raw === "" || raw === undefined;
|
|
221
|
+
|
|
222
|
+
if (isEmpty) {
|
|
223
|
+
// No error message shown for an empty "Other" field — just mark invalid silently
|
|
224
|
+
const newErrors = { ...this.errors };
|
|
225
|
+
delete newErrors[fee.id];
|
|
226
|
+
this.errors = newErrors;
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!v || v <= 0) {
|
|
231
|
+
const newErrors = { ...this.errors };
|
|
232
|
+
delete newErrors[fee.id];
|
|
233
|
+
this.errors = newErrors;
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (fee.min_amount != null && v < Number(fee.min_amount)) {
|
|
238
|
+
this.errors = { ...this.errors, [fee.id]: `Minimum is $${Number(fee.min_amount)}.` };
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
if (fee.max_amount != null && v > Number(fee.max_amount)) {
|
|
242
|
+
this.errors = { ...this.errors, [fee.id]: `Maximum is $${Number(fee.max_amount)}.` };
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
const newErrors = { ...this.errors };
|
|
246
|
+
delete newErrors[fee.id];
|
|
247
|
+
this.errors = newErrors;
|
|
248
|
+
return true;
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
amountFor(fee) {
|
|
252
|
+
if (!this.isChecked(fee)) return 0;
|
|
253
|
+
let amount = 0;
|
|
254
|
+
const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase();
|
|
255
|
+
if (calc.includes("percentage")) {
|
|
256
|
+
amount = (this.subtotal * Number(fee.value || 0)) / 100;
|
|
257
|
+
} else if (this.isCustomerAmount(fee)) {
|
|
258
|
+
amount = Number(this.selection[fee.id] || 0);
|
|
259
|
+
} else {
|
|
260
|
+
amount = Number(fee.value || 0);
|
|
261
|
+
}
|
|
262
|
+
if (fee.min_amount != null) amount = Math.max(amount, Number(fee.min_amount));
|
|
263
|
+
if (fee.max_amount != null) amount = Math.min(amount, Number(fee.max_amount));
|
|
264
|
+
return Math.round(Math.max(amount, 0) * 100) / 100;
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
label(fee) {
|
|
268
|
+
if (this.isCustomerAmount(fee)) {
|
|
269
|
+
return this.isChecked(fee) ? `${this.currency}${this.amountFor(fee).toFixed(2)}` : "";
|
|
270
|
+
}
|
|
271
|
+
const calc = String(fee.calculation || fee.calculation_type || fee.type || "").toLowerCase();
|
|
272
|
+
if (calc.includes("percentage")) {
|
|
273
|
+
return `${Number(fee.value)}%`;
|
|
274
|
+
}
|
|
275
|
+
return `${this.currency}${Number(fee.value || 0).toFixed(2)}`;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
emitChange() {
|
|
279
|
+
const payload = Object.entries(this.selection).map(([id, amount]) => ({
|
|
280
|
+
extra_fee_id: id,
|
|
281
|
+
customer_amount: amount,
|
|
282
|
+
}));
|
|
283
|
+
const total = this.fees.reduce((sum, f) => sum + this.amountFor(f), 0);
|
|
284
|
+
const valid = this.fees.every((f) => !this.isChecked(f) || this.validate(f));
|
|
285
|
+
this.$emit("change", {
|
|
286
|
+
payload,
|
|
287
|
+
total: Math.round(total * 100) / 100,
|
|
288
|
+
valid,
|
|
289
|
+
lines: this.fees
|
|
290
|
+
.filter((f) => this.isChecked(f))
|
|
291
|
+
.map((f) => ({ name: f.name, amount: this.amountFor(f) })),
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
seedRequired() {
|
|
296
|
+
this.fees.forEach((f) => {
|
|
297
|
+
if (f.is_required && !this.isChecked(f)) {
|
|
298
|
+
if (this.isCustomerAmount(f)) {
|
|
299
|
+
const presets = this.getPresets(f);
|
|
300
|
+
const defaultPreset = presets.length ? presets[0] : (Number(f.min_amount) || 5);
|
|
301
|
+
this.selection = { ...this.selection, [f.id]: defaultPreset };
|
|
302
|
+
this.selectedMode = { ...this.selectedMode, [f.id]: "preset" };
|
|
303
|
+
} else {
|
|
304
|
+
this.selection = { ...this.selection, [f.id]: null };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
this.emitChange();
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
watch: {
|
|
312
|
+
fees: { handler() { this.seedRequired(); }, immediate: true },
|
|
313
|
+
subtotal() { this.emitChange(); },
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
</script>
|
|
317
|
+
<style scoped>
|
|
318
|
+
.osi-fee-currency {
|
|
319
|
+
position: absolute;
|
|
320
|
+
inset-inline-start: var(--osi-input-pad-x, 12px);
|
|
321
|
+
z-index: 1;
|
|
322
|
+
font-size: 0.875rem;
|
|
323
|
+
opacity: 0.6;
|
|
324
|
+
pointer-events: none;
|
|
325
|
+
}
|
|
326
|
+
.osi-fee-amount {
|
|
327
|
+
--in-pad-x: calc(var(--osi-input-pad-x, 12px) + 0.75rem);
|
|
328
|
+
}
|
|
329
|
+
</style>
|