@mundogamernetwork/shared-ui 1.16.27 → 1.17.0
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/components/checkout/MgBillingProfileFields.vue +174 -0
- package/components/keys/KeyBrowser.vue +4 -4
- package/composables/useMgBillingProfile.ts +99 -0
- package/composables/useMgCheckout.ts +29 -0
- package/locales/de.json +16 -0
- package/locales/en.json +16 -0
- package/locales/es.json +16 -0
- package/locales/pt-BR.json +16 -0
- package/locales/ro.json +16 -0
- package/package.json +1 -1
- package/pages/key-campaigns/key-materials.vue +27 -13
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { ref, watch, onMounted } from "vue";
|
|
3
|
+
import type { AxiosInstance } from "axios";
|
|
4
|
+
import { useMgBillingProfile } from "../../composables/useMgBillingProfile";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* "I need an invoice with company details" — the fiscal block every checkout
|
|
8
|
+
* needs.
|
|
9
|
+
*
|
|
10
|
+
* The data is stored once on the customer's profile and snapshotted onto every
|
|
11
|
+
* document issued afterwards, so asking here covers the purchase being made,
|
|
12
|
+
* every renewal of it, and every later one-off purchase too.
|
|
13
|
+
*/
|
|
14
|
+
const props = defineProps<{
|
|
15
|
+
httpService: AxiosInstance;
|
|
16
|
+
/** Start expanded regardless of whether a profile already exists. */
|
|
17
|
+
defaultOpen?: boolean;
|
|
18
|
+
}>();
|
|
19
|
+
|
|
20
|
+
const emit = defineEmits<{ (e: "change", wanted: boolean): void }>();
|
|
21
|
+
|
|
22
|
+
const billing = useMgBillingProfile(props.httpService);
|
|
23
|
+
const wanted = ref(props.defaultOpen ?? false);
|
|
24
|
+
|
|
25
|
+
// A customer who already gave us their details should see them, ticked — not
|
|
26
|
+
// an empty box that reads as the platform having forgotten.
|
|
27
|
+
watch(
|
|
28
|
+
() => billing.hasData.value,
|
|
29
|
+
(hasData) => {
|
|
30
|
+
if (hasData) wanted.value = true;
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
watch(wanted, (value) => emit("change", value));
|
|
35
|
+
|
|
36
|
+
onMounted(billing.load);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Persist the details, if the customer asked for them.
|
|
40
|
+
*
|
|
41
|
+
* @returns false only when a requested save actually failed, so a caller can
|
|
42
|
+
* hold back the gateway redirect instead of sending the customer off
|
|
43
|
+
* with details that were never stored.
|
|
44
|
+
*/
|
|
45
|
+
async function save(): Promise<boolean> {
|
|
46
|
+
if (!wanted.value) return true;
|
|
47
|
+
return billing.save();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
defineExpose({ save, wanted, profile: billing.profile, hasData: billing.hasData });
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<div class="mg-billing-profile">
|
|
55
|
+
<label class="mg-billing-profile__toggle">
|
|
56
|
+
<input v-model="wanted" type="checkbox" />
|
|
57
|
+
<span>{{ $t?.("billing_profile.toggle") ?? "I need an invoice with company details" }}</span>
|
|
58
|
+
</label>
|
|
59
|
+
|
|
60
|
+
<div v-show="wanted" class="mg-billing-profile__body">
|
|
61
|
+
<p class="mg-billing-profile__hint">{{ $t?.("billing_profile.hint") ?? "Saved to your account and used on every invoice from now on, including each renewal." }}</p>
|
|
62
|
+
|
|
63
|
+
<div class="mg-billing-profile__grid">
|
|
64
|
+
<label class="mg-billing-profile__field mg-billing-profile__field--wide">
|
|
65
|
+
<span>{{ $t?.("billing_profile.company_name") ?? "Company name" }}</span>
|
|
66
|
+
<input v-model="billing.profile.value.company_name" type="text" />
|
|
67
|
+
</label>
|
|
68
|
+
<label class="mg-billing-profile__field mg-billing-profile__field--wide">
|
|
69
|
+
<span>{{ $t?.("billing_profile.legal_name") ?? "Legal name" }}</span>
|
|
70
|
+
<input v-model="billing.profile.value.legal_name" type="text" />
|
|
71
|
+
</label>
|
|
72
|
+
<label class="mg-billing-profile__field">
|
|
73
|
+
<span>{{ $t?.("billing_profile.vat_number") ?? "VAT number" }}</span>
|
|
74
|
+
<input v-model="billing.profile.value.vat_number" type="text" />
|
|
75
|
+
</label>
|
|
76
|
+
<label class="mg-billing-profile__field">
|
|
77
|
+
<span>{{ $t?.("billing_profile.tax_id") ?? "Tax ID" }}</span>
|
|
78
|
+
<input v-model="billing.profile.value.tax_id" type="text" />
|
|
79
|
+
</label>
|
|
80
|
+
<label class="mg-billing-profile__field">
|
|
81
|
+
<span>{{ $t?.("billing_profile.registration_number") ?? "Registration number" }}</span>
|
|
82
|
+
<input v-model="billing.profile.value.registration_number" type="text" />
|
|
83
|
+
</label>
|
|
84
|
+
<label class="mg-billing-profile__field">
|
|
85
|
+
<span>{{ $t?.("billing_profile.represented_by") ?? "Represented by (director)" }}</span>
|
|
86
|
+
<input v-model="billing.profile.value.represented_by" type="text" />
|
|
87
|
+
</label>
|
|
88
|
+
<label class="mg-billing-profile__field mg-billing-profile__field--wide">
|
|
89
|
+
<span>{{ $t?.("billing_profile.address") ?? "Address" }}</span>
|
|
90
|
+
<input v-model="billing.profile.value.address_line_1" type="text" />
|
|
91
|
+
</label>
|
|
92
|
+
<label class="mg-billing-profile__field">
|
|
93
|
+
<span>{{ $t?.("billing_profile.city") ?? "City" }}</span>
|
|
94
|
+
<input v-model="billing.profile.value.city" type="text" />
|
|
95
|
+
</label>
|
|
96
|
+
<label class="mg-billing-profile__field">
|
|
97
|
+
<span>{{ $t?.("billing_profile.postal_code") ?? "Postal code" }}</span>
|
|
98
|
+
<input v-model="billing.profile.value.postal_code" type="text" />
|
|
99
|
+
</label>
|
|
100
|
+
<label class="mg-billing-profile__field">
|
|
101
|
+
<span>{{ $t?.("billing_profile.country") ?? "Country" }}</span>
|
|
102
|
+
<input v-model="billing.profile.value.country" type="text" />
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<p v-if="billing.error.value" class="mg-billing-profile__error">
|
|
107
|
+
{{ billing.error.value || $t?.("billing_profile.save_error") ?? "Could not save your billing details. Please try again." }}
|
|
108
|
+
</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<style lang="scss" scoped>
|
|
114
|
+
.mg-billing-profile {
|
|
115
|
+
margin-top: 16px;
|
|
116
|
+
border-top: 1px solid var(--inactive, #ececec);
|
|
117
|
+
padding-top: 16px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.mg-billing-profile__toggle {
|
|
121
|
+
display: flex;
|
|
122
|
+
align-items: center;
|
|
123
|
+
gap: 8px;
|
|
124
|
+
font-size: 14px;
|
|
125
|
+
cursor: pointer;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.mg-billing-profile__body {
|
|
129
|
+
margin-top: 14px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.mg-billing-profile__hint {
|
|
133
|
+
font-size: 13px;
|
|
134
|
+
color: var(--inactive-text, #666);
|
|
135
|
+
margin: 0 0 12px;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.mg-billing-profile__grid {
|
|
139
|
+
display: grid;
|
|
140
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
141
|
+
gap: 12px;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.mg-billing-profile__field {
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
gap: 4px;
|
|
148
|
+
font-size: 12px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.mg-billing-profile__field--wide {
|
|
152
|
+
grid-column: 1 / -1;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.mg-billing-profile__field input {
|
|
156
|
+
border: 1px solid var(--inactive, #d5d5d5);
|
|
157
|
+
padding: 8px 10px;
|
|
158
|
+
font-size: 14px;
|
|
159
|
+
background: var(--bg, #fff);
|
|
160
|
+
color: var(--active, #111);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.mg-billing-profile__error {
|
|
164
|
+
margin: 10px 0 0;
|
|
165
|
+
color: var(--error, #ee3831);
|
|
166
|
+
font-size: 13px;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@media (max-width: 620px) {
|
|
170
|
+
.mg-billing-profile__grid {
|
|
171
|
+
grid-template-columns: 1fr;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
</style>
|
|
@@ -69,7 +69,7 @@ async function loadPools() {
|
|
|
69
69
|
loadingPools.value = true
|
|
70
70
|
try {
|
|
71
71
|
const res = await fetchAvailablePools(queryParams())
|
|
72
|
-
pools.value = (res as any)?.data ?? []
|
|
72
|
+
pools.value = (res as any)?.data?.data ?? []
|
|
73
73
|
pools.value.forEach(p => {
|
|
74
74
|
if (!requestForms.value[p.id]) {
|
|
75
75
|
requestForms.value[p.id] = { platform_ids: [], description: '' }
|
|
@@ -88,7 +88,7 @@ async function loadMyRequests() {
|
|
|
88
88
|
loadingRequests.value = true
|
|
89
89
|
try {
|
|
90
90
|
const res = await fetchMyRequests()
|
|
91
|
-
myRequests.value = (res as any)?.data ?? []
|
|
91
|
+
myRequests.value = (res as any)?.data?.data ?? []
|
|
92
92
|
} catch (err: any) {
|
|
93
93
|
if (err?.response?.status === 401) isAuthenticated.value = false
|
|
94
94
|
} finally {
|
|
@@ -99,7 +99,7 @@ async function loadMyRequests() {
|
|
|
99
99
|
async function loadPlatformsData() {
|
|
100
100
|
try {
|
|
101
101
|
const res = await fetchPlatforms()
|
|
102
|
-
platforms.value = (res as any)?.data ?? []
|
|
102
|
+
platforms.value = (res as any)?.data?.data ?? []
|
|
103
103
|
} catch {}
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -218,7 +218,7 @@ async function revealCode(reqId: number) {
|
|
|
218
218
|
const body: Record<string, any> = {}
|
|
219
219
|
if (props.accessToken) body.access_token = props.accessToken
|
|
220
220
|
const res = await apiRevealKey(reqId, body)
|
|
221
|
-
revealResult.value = (res as any)?.data ?? null
|
|
221
|
+
revealResult.value = (res as any)?.data?.data ?? null
|
|
222
222
|
} catch (err: any) {
|
|
223
223
|
if (err?.response?.status === 401) isAuthenticated.value = false
|
|
224
224
|
} finally {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ref, computed } from "vue";
|
|
2
|
+
import type { AxiosInstance } from "axios";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The customer's fiscal identity — company name, tax ids, address, the person
|
|
6
|
+
* who signs for the company.
|
|
7
|
+
*
|
|
8
|
+
* It is stored once per customer and snapshotted onto every document the
|
|
9
|
+
* platform issues them afterwards (api-main does that on Invoice and on
|
|
10
|
+
* PlanSubscriptionPayment creation). Collecting it at checkout is what stops a
|
|
11
|
+
* subscription from producing an unusable receipt every single billing cycle:
|
|
12
|
+
* the payment record is created on the way back from the gateway, so anything
|
|
13
|
+
* asked for afterwards misses at least the first document.
|
|
14
|
+
*/
|
|
15
|
+
const BILLING_PROFILE_URL = "api/v1/public/user/billing-profile";
|
|
16
|
+
|
|
17
|
+
export interface MgBillingProfile {
|
|
18
|
+
name: string;
|
|
19
|
+
company_name: string;
|
|
20
|
+
legal_name: string;
|
|
21
|
+
vat_number: string;
|
|
22
|
+
tax_id: string;
|
|
23
|
+
registration_number: string;
|
|
24
|
+
represented_by: string;
|
|
25
|
+
address_line_1: string;
|
|
26
|
+
address_line_2: string;
|
|
27
|
+
city: string;
|
|
28
|
+
state: string;
|
|
29
|
+
postal_code: string;
|
|
30
|
+
country: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function emptyMgBillingProfile(): MgBillingProfile {
|
|
34
|
+
return {
|
|
35
|
+
name: "",
|
|
36
|
+
company_name: "",
|
|
37
|
+
legal_name: "",
|
|
38
|
+
vat_number: "",
|
|
39
|
+
tax_id: "",
|
|
40
|
+
registration_number: "",
|
|
41
|
+
represented_by: "",
|
|
42
|
+
address_line_1: "",
|
|
43
|
+
address_line_2: "",
|
|
44
|
+
city: "",
|
|
45
|
+
state: "",
|
|
46
|
+
postal_code: "",
|
|
47
|
+
country: "",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function useMgBillingProfile(httpService: AxiosInstance) {
|
|
52
|
+
const profile = ref<MgBillingProfile>(emptyMgBillingProfile());
|
|
53
|
+
const loading = ref(false);
|
|
54
|
+
const saving = ref(false);
|
|
55
|
+
const error = ref<string | null>(null);
|
|
56
|
+
|
|
57
|
+
/** Whether the customer has given us anything worth printing on a document. */
|
|
58
|
+
const hasData = computed(() =>
|
|
59
|
+
Object.values(profile.value).some((value) => String(value ?? "").trim() !== "")
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
async function load() {
|
|
63
|
+
loading.value = true;
|
|
64
|
+
try {
|
|
65
|
+
const res = await httpService.get(BILLING_PROFILE_URL);
|
|
66
|
+
const data = res?.data?.data ?? {};
|
|
67
|
+
(Object.keys(profile.value) as Array<keyof MgBillingProfile>).forEach((key) => {
|
|
68
|
+
profile.value[key] = data[key] ?? "";
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
// A profile that cannot be read just starts empty — never a reason to
|
|
72
|
+
// block a checkout the customer is in the middle of.
|
|
73
|
+
} finally {
|
|
74
|
+
loading.value = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @returns whether the save succeeded, so a caller can hold back a gateway
|
|
80
|
+
* redirect rather than sending the customer off with details that
|
|
81
|
+
* were never stored.
|
|
82
|
+
*/
|
|
83
|
+
async function save(): Promise<boolean> {
|
|
84
|
+
if (saving.value) return false;
|
|
85
|
+
saving.value = true;
|
|
86
|
+
error.value = null;
|
|
87
|
+
try {
|
|
88
|
+
await httpService.put(BILLING_PROFILE_URL, profile.value);
|
|
89
|
+
return true;
|
|
90
|
+
} catch (err: any) {
|
|
91
|
+
error.value = err?.response?.data?.message ?? null;
|
|
92
|
+
return false;
|
|
93
|
+
} finally {
|
|
94
|
+
saving.value = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { profile, loading, saving, error, hasData, load, save };
|
|
99
|
+
}
|
|
@@ -307,12 +307,37 @@ export function useMgCheckout(httpService: AxiosInstance, options?: {
|
|
|
307
307
|
|
|
308
308
|
// ── startValidation: the actual checkout submission ────────────────────────
|
|
309
309
|
|
|
310
|
+
// ── Fiscal data ────────────────────────────────────────────────────────────
|
|
311
|
+
// A checkout page registers its MgBillingProfileFields here; the details are
|
|
312
|
+
// then saved before the gateway redirect. That ordering is the whole point:
|
|
313
|
+
// the payment record is created on the way back and snapshots whatever the
|
|
314
|
+
// profile holds at that moment, so anything collected afterwards misses the
|
|
315
|
+
// document the customer is buying right now — and on a subscription it
|
|
316
|
+
// misses every cycle until somebody notices.
|
|
317
|
+
const billingProfileFields = ref<{ save: () => Promise<boolean> } | null>(null);
|
|
318
|
+
|
|
319
|
+
function registerBillingProfileFields(instance: { save: () => Promise<boolean> } | null) {
|
|
320
|
+
billingProfileFields.value = instance;
|
|
321
|
+
}
|
|
322
|
+
|
|
310
323
|
async function startValidation() {
|
|
311
324
|
if (loading.value) return;
|
|
312
325
|
try {
|
|
313
326
|
loading.value = true;
|
|
314
327
|
if (buyerPageName) localStorage.setItem("buyerPage", buyerPageName);
|
|
315
328
|
|
|
329
|
+
if (billingProfileFields.value) {
|
|
330
|
+
const saved = await billingProfileFields.value.save();
|
|
331
|
+
if (!saved) {
|
|
332
|
+
checkoutError.value = _t(
|
|
333
|
+
"billing_profile.save_error",
|
|
334
|
+
"Could not save your billing details. Please try again."
|
|
335
|
+
);
|
|
336
|
+
loading.value = false;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
316
341
|
const gatewayStr = selectedPaymentGateway.value || "stripe";
|
|
317
342
|
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
318
343
|
const localeStr = locale.value || "en";
|
|
@@ -523,6 +548,10 @@ export function useMgCheckout(httpService: AxiosInstance, options?: {
|
|
|
523
548
|
emailError,
|
|
524
549
|
isInvalid,
|
|
525
550
|
|
|
551
|
+
// Fiscal data
|
|
552
|
+
billingProfileFields,
|
|
553
|
+
registerBillingProfileFields,
|
|
554
|
+
|
|
526
555
|
// Actions
|
|
527
556
|
handlePurchase,
|
|
528
557
|
startValidation,
|
package/locales/de.json
CHANGED
|
@@ -449,6 +449,7 @@
|
|
|
449
449
|
"required_fields": "Fill in all required fields",
|
|
450
450
|
"url_error": "Invalid URL",
|
|
451
451
|
"submit_error": "Error submitting. Try again.",
|
|
452
|
+
"duplicate_url_error": "Du hast diesen Link für diese Anfrage bereits gesendet.",
|
|
452
453
|
"sending": "Sending...",
|
|
453
454
|
"finish": "Submit",
|
|
454
455
|
"submitted_title": "Material submitted!",
|
|
@@ -586,5 +587,20 @@
|
|
|
586
587
|
"tiers": "Packages"
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
},
|
|
591
|
+
"billing_profile": {
|
|
592
|
+
"toggle": "Ich benötige eine Rechnung mit Firmendaten",
|
|
593
|
+
"hint": "Wird in Ihrem Konto gespeichert und ab sofort auf jeder Rechnung verwendet, auch bei jeder Verlängerung.",
|
|
594
|
+
"company_name": "Firmenname",
|
|
595
|
+
"legal_name": "Rechtlicher Name",
|
|
596
|
+
"vat_number": "USt-IdNr.",
|
|
597
|
+
"tax_id": "Steuernummer",
|
|
598
|
+
"registration_number": "Registernummer",
|
|
599
|
+
"represented_by": "Vertreten durch (Geschäftsführer)",
|
|
600
|
+
"address": "Anschrift",
|
|
601
|
+
"city": "Stadt",
|
|
602
|
+
"postal_code": "Postleitzahl",
|
|
603
|
+
"country": "Land",
|
|
604
|
+
"save_error": "Ihre Rechnungsdaten konnten nicht gespeichert werden. Bitte versuchen Sie es erneut."
|
|
589
605
|
}
|
|
590
606
|
}
|
package/locales/en.json
CHANGED
|
@@ -449,6 +449,7 @@
|
|
|
449
449
|
"required_fields": "Fill in all required fields",
|
|
450
450
|
"url_error": "Invalid URL",
|
|
451
451
|
"submit_error": "Error submitting. Try again.",
|
|
452
|
+
"duplicate_url_error": "You already sent this link for this request.",
|
|
452
453
|
"sending": "Sending...",
|
|
453
454
|
"finish": "Submit",
|
|
454
455
|
"submitted_title": "Material submitted!",
|
|
@@ -586,5 +587,20 @@
|
|
|
586
587
|
"tiers": "Packages"
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
},
|
|
591
|
+
"billing_profile": {
|
|
592
|
+
"toggle": "I need an invoice with company details",
|
|
593
|
+
"hint": "Saved to your account and used on every invoice from now on, including each renewal.",
|
|
594
|
+
"company_name": "Company name",
|
|
595
|
+
"legal_name": "Legal name",
|
|
596
|
+
"vat_number": "VAT number",
|
|
597
|
+
"tax_id": "Tax ID",
|
|
598
|
+
"registration_number": "Registration number",
|
|
599
|
+
"represented_by": "Represented by (director)",
|
|
600
|
+
"address": "Address",
|
|
601
|
+
"city": "City",
|
|
602
|
+
"postal_code": "Postal code",
|
|
603
|
+
"country": "Country",
|
|
604
|
+
"save_error": "Could not save your billing details. Please try again."
|
|
589
605
|
}
|
|
590
606
|
}
|
package/locales/es.json
CHANGED
|
@@ -449,6 +449,7 @@
|
|
|
449
449
|
"required_fields": "¡Por favor, completa todos los campos obligatorios!",
|
|
450
450
|
"url_error": "Introduce URLs válidas (deben comenzar con http:// o https://)",
|
|
451
451
|
"submit_error": "Hubo un error al procesar tu material.",
|
|
452
|
+
"duplicate_url_error": "Ya enviaste este enlace para esta solicitud.",
|
|
452
453
|
"sending": "Enviando...",
|
|
453
454
|
"finish": "Finalizar",
|
|
454
455
|
"submitted_title": "Material Enviado",
|
|
@@ -586,5 +587,20 @@
|
|
|
586
587
|
"tiers": "Paquetes de Apoyo"
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
},
|
|
591
|
+
"billing_profile": {
|
|
592
|
+
"toggle": "Necesito factura con datos de empresa",
|
|
593
|
+
"hint": "Se guarda en tu cuenta y aparece en todas las facturas a partir de ahora, incluidas las renovaciones.",
|
|
594
|
+
"company_name": "Razón social",
|
|
595
|
+
"legal_name": "Nombre legal",
|
|
596
|
+
"vat_number": "Número de VAT",
|
|
597
|
+
"tax_id": "Identificación fiscal",
|
|
598
|
+
"registration_number": "Número de registro",
|
|
599
|
+
"represented_by": "Representada por (director)",
|
|
600
|
+
"address": "Dirección",
|
|
601
|
+
"city": "Ciudad",
|
|
602
|
+
"postal_code": "Código postal",
|
|
603
|
+
"country": "País",
|
|
604
|
+
"save_error": "No se pudieron guardar tus datos de facturación. Inténtalo de nuevo."
|
|
589
605
|
}
|
|
590
606
|
}
|
package/locales/pt-BR.json
CHANGED
|
@@ -449,6 +449,7 @@
|
|
|
449
449
|
"required_fields": "Fill in all required fields",
|
|
450
450
|
"url_error": "Invalid URL",
|
|
451
451
|
"submit_error": "Error submitting. Try again.",
|
|
452
|
+
"duplicate_url_error": "Você já enviou esse link para este pedido.",
|
|
452
453
|
"sending": "Sending...",
|
|
453
454
|
"finish": "Submit",
|
|
454
455
|
"submitted_title": "Material submitted!",
|
|
@@ -586,5 +587,20 @@
|
|
|
586
587
|
"tiers": "Pacotes"
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
},
|
|
591
|
+
"billing_profile": {
|
|
592
|
+
"toggle": "Preciso de nota com dados da empresa",
|
|
593
|
+
"hint": "Fica salvo na sua conta e sai em todas as próximas faturas, inclusive nas renovações.",
|
|
594
|
+
"company_name": "Razão social",
|
|
595
|
+
"legal_name": "Nome legal",
|
|
596
|
+
"vat_number": "CNPJ / VAT",
|
|
597
|
+
"tax_id": "Inscrição fiscal",
|
|
598
|
+
"registration_number": "Número de registro",
|
|
599
|
+
"represented_by": "Representada por (diretor)",
|
|
600
|
+
"address": "Endereço",
|
|
601
|
+
"city": "Cidade",
|
|
602
|
+
"postal_code": "CEP",
|
|
603
|
+
"country": "País",
|
|
604
|
+
"save_error": "Não foi possível salvar seus dados de faturamento. Tente novamente."
|
|
589
605
|
}
|
|
590
606
|
}
|
package/locales/ro.json
CHANGED
|
@@ -449,6 +449,7 @@
|
|
|
449
449
|
"required_fields": "Fill in all required fields",
|
|
450
450
|
"url_error": "Invalid URL",
|
|
451
451
|
"submit_error": "Error submitting. Try again.",
|
|
452
|
+
"duplicate_url_error": "Ai trimis deja acest link pentru această solicitare.",
|
|
452
453
|
"sending": "Sending...",
|
|
453
454
|
"finish": "Submit",
|
|
454
455
|
"submitted_title": "Material submitted!",
|
|
@@ -586,5 +587,20 @@
|
|
|
586
587
|
"tiers": "Packages"
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
},
|
|
591
|
+
"billing_profile": {
|
|
592
|
+
"toggle": "Am nevoie de factură cu datele firmei",
|
|
593
|
+
"hint": "Se salvează în contul tău și apare pe toate facturile de acum înainte, inclusiv la reînnoiri.",
|
|
594
|
+
"company_name": "Denumirea firmei",
|
|
595
|
+
"legal_name": "Denumire legală",
|
|
596
|
+
"vat_number": "Cod TVA",
|
|
597
|
+
"tax_id": "Identificator fiscal",
|
|
598
|
+
"registration_number": "Număr de înregistrare",
|
|
599
|
+
"represented_by": "Reprezentată de (director)",
|
|
600
|
+
"address": "Adresă",
|
|
601
|
+
"city": "Oraș",
|
|
602
|
+
"postal_code": "Cod poștal",
|
|
603
|
+
"country": "Țară",
|
|
604
|
+
"save_error": "Nu am putut salva datele tale de facturare. Încearcă din nou."
|
|
589
605
|
}
|
|
590
606
|
}
|
package/package.json
CHANGED
|
@@ -352,19 +352,33 @@ const submitForm = async () => {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
for (const form of forms.value) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
355
|
+
try {
|
|
356
|
+
await submitMaterial({
|
|
357
|
+
key_request_id: requestId.value,
|
|
358
|
+
user_id: unref(currentUserId),
|
|
359
|
+
// Slugs, not guessed ids. article/video/stream/web/print already
|
|
360
|
+
// match content_types.slug (Spatie\Sluggable on the model's own
|
|
361
|
+
// `name`) — the backend resolves them, which is what makes this
|
|
362
|
+
// work regardless of what numeric ids that table happens to hold
|
|
363
|
+
// in a given environment.
|
|
364
|
+
content_types: form.content_types,
|
|
365
|
+
material_url: form.material_url,
|
|
366
|
+
description: form.description,
|
|
367
|
+
submitted_at: new Date(form.submitted_at).toISOString(),
|
|
368
|
+
})
|
|
369
|
+
} catch (submitError: any) {
|
|
370
|
+
// Returning to this page later (or double-submitting) used to
|
|
371
|
+
// silently create a second identical row — the backend now
|
|
372
|
+
// rejects the exact link a second time per key request, but
|
|
373
|
+
// without catching this specific code here it fell through to
|
|
374
|
+
// the generic "something went wrong" below, leaving the person
|
|
375
|
+
// no wiser that it's the SAME link they already sent.
|
|
376
|
+
if (submitError?.response?.data?.error === "DUPLICATE_MATERIAL_URL") {
|
|
377
|
+
formError.value = t("keys.materials.duplicate_url_error")
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
throw submitError
|
|
381
|
+
}
|
|
368
382
|
}
|
|
369
383
|
|
|
370
384
|
// Reload submitted list
|