Phase6c: Optional Company contact name
All checks were successful
Build and Push / build (push) Successful in 1m38s
All checks were successful
Build and Push / build (push) Successful in 1m38s
This commit is contained in:
@@ -76,6 +76,7 @@ export default async function NewInstancePage() {
|
|||||||
userName={user.name}
|
userName={user.name}
|
||||||
userEmail={user.email}
|
userEmail={user.email}
|
||||||
hasOrgBilling={hasOrgBilling}
|
hasOrgBilling={hasOrgBilling}
|
||||||
|
existingOrgBilling={orgBilling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ export default async function DashboardPage() {
|
|||||||
userName={user.name}
|
userName={user.name}
|
||||||
userEmail={user.email}
|
userEmail={user.email}
|
||||||
hasOrgBilling={hasOrgBilling}
|
hasOrgBilling={hasOrgBilling}
|
||||||
|
existingOrgBilling={orgBilling}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { OnboardingWizard } from "./wizard";
|
import { OnboardingWizard } from "./wizard";
|
||||||
|
import type { OrgBilling } from "@/types";
|
||||||
|
|
||||||
interface OnboardingFlowProps {
|
interface OnboardingFlowProps {
|
||||||
orgName: string;
|
orgName: string;
|
||||||
@@ -19,6 +20,12 @@ interface OnboardingFlowProps {
|
|||||||
* /settings/billing.
|
* /settings/billing.
|
||||||
*/
|
*/
|
||||||
hasOrgBilling?: boolean;
|
hasOrgBilling?: boolean;
|
||||||
|
/**
|
||||||
|
* Phase 6 fix3: the actual org_billing record (or null). Drives
|
||||||
|
* the review-step "Billing to" rendering AND the confirm-step
|
||||||
|
* validation skip when the billing step was skipped.
|
||||||
|
*/
|
||||||
|
existingOrgBilling?: OrgBilling | null;
|
||||||
/**
|
/**
|
||||||
* Bug 6: when present, the wizard is rendered in edit mode against
|
* Bug 6: when present, the wizard is rendered in edit mode against
|
||||||
* the given pending request. See `OnboardingWizard` for the full
|
* the given pending request. See `OnboardingWizard` for the full
|
||||||
@@ -45,6 +52,7 @@ export function OnboardingFlow({
|
|||||||
userName,
|
userName,
|
||||||
userEmail,
|
userEmail,
|
||||||
hasOrgBilling,
|
hasOrgBilling,
|
||||||
|
existingOrgBilling,
|
||||||
editingRequest,
|
editingRequest,
|
||||||
}: OnboardingFlowProps) {
|
}: OnboardingFlowProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -55,6 +63,7 @@ export function OnboardingFlow({
|
|||||||
userName={userName}
|
userName={userName}
|
||||||
userEmail={userEmail}
|
userEmail={userEmail}
|
||||||
hasOrgBilling={hasOrgBilling}
|
hasOrgBilling={hasOrgBilling}
|
||||||
|
existingOrgBilling={existingOrgBilling}
|
||||||
editingRequest={editingRequest}
|
editingRequest={editingRequest}
|
||||||
onComplete={() => {
|
onComplete={() => {
|
||||||
// Navigate back to /dashboard and re-fetch on the server. The
|
// Navigate back to /dashboard and re-fetch on the server. The
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
SUPPORTED_COUNTRIES,
|
SUPPORTED_COUNTRIES,
|
||||||
type SupportedCountry,
|
type SupportedCountry,
|
||||||
} from "@/lib/validation";
|
} from "@/lib/validation";
|
||||||
|
import type { OrgBilling } from "@/types";
|
||||||
|
|
||||||
type Step = "welcome" | "configure" | "billing" | "confirm";
|
type Step = "welcome" | "configure" | "billing" | "confirm";
|
||||||
|
|
||||||
@@ -96,6 +97,17 @@ interface WizardProps {
|
|||||||
* fix it before admin approves.
|
* fix it before admin approves.
|
||||||
*/
|
*/
|
||||||
hasOrgBilling?: boolean;
|
hasOrgBilling?: boolean;
|
||||||
|
/**
|
||||||
|
* Phase 6 fix3: the actual org_billing record when one exists.
|
||||||
|
* Used to render real values on the review-step "Billing to" block
|
||||||
|
* (rather than the wizard's empty default config.billingAddress)
|
||||||
|
* AND to skip the confirm-step's client-side validation of
|
||||||
|
* billingAddress — same logic that already strips billingAddress
|
||||||
|
* at submit time. Null when no org_billing row exists yet.
|
||||||
|
* Ignored in edit mode (the editingRequest carries its own
|
||||||
|
* billingAddress snapshot).
|
||||||
|
*/
|
||||||
|
existingOrgBilling?: OrgBilling | null;
|
||||||
/**
|
/**
|
||||||
* Bug 6: when present, the wizard renders in "edit" mode — fields
|
* Bug 6: when present, the wizard renders in "edit" mode — fields
|
||||||
* are pre-populated from the request, the SOUL.md auto-fetch is
|
* are pre-populated from the request, the SOUL.md auto-fetch is
|
||||||
@@ -134,6 +146,7 @@ export function OnboardingWizard({
|
|||||||
userName,
|
userName,
|
||||||
userEmail,
|
userEmail,
|
||||||
hasOrgBilling,
|
hasOrgBilling,
|
||||||
|
existingOrgBilling,
|
||||||
editingRequest,
|
editingRequest,
|
||||||
onComplete,
|
onComplete,
|
||||||
}: WizardProps) {
|
}: WizardProps) {
|
||||||
@@ -319,7 +332,23 @@ export function OnboardingWizard({
|
|||||||
}
|
}
|
||||||
// confirm: validate the union (defence in depth — submit handler
|
// confirm: validate the union (defence in depth — submit handler
|
||||||
// also runs onboardingSchema before POST).
|
// also runs onboardingSchema before POST).
|
||||||
const r = onboardingSchema.safeParse(config);
|
//
|
||||||
|
// Phase 6 fix3: when hasOrgBilling=true AND not editing, the
|
||||||
|
// billing step was skipped and config.billingAddress is the
|
||||||
|
// empty default. zod's .optional() doesn't help here because the
|
||||||
|
// field IS present (empty object), so billingAddressSchema
|
||||||
|
// validates it and fails with required-field errors that the
|
||||||
|
// user has no way to fix — the form to enter the values was
|
||||||
|
// skipped on purpose. Strip the field for validation, matching
|
||||||
|
// the same strip we already do at submit time.
|
||||||
|
const configForValidation =
|
||||||
|
hasOrgBilling && !isEditing
|
||||||
|
? (() => {
|
||||||
|
const { billingAddress: _b, ...rest } = config;
|
||||||
|
return rest;
|
||||||
|
})()
|
||||||
|
: config;
|
||||||
|
const r = onboardingSchema.safeParse(configForValidation);
|
||||||
if (r.success) {
|
if (r.success) {
|
||||||
setErrors({});
|
setErrors({});
|
||||||
return true;
|
return true;
|
||||||
@@ -1101,42 +1130,84 @@ export function OnboardingWizard({
|
|||||||
<ReviewRow
|
<ReviewRow
|
||||||
label={t("reviewBillingTo")}
|
label={t("reviewBillingTo")}
|
||||||
value={
|
value={
|
||||||
<div className="text-text-primary text-right">
|
(() => {
|
||||||
{/* For personal: skip the company line so the
|
// Phase 6 fix3: when the org has billing on file
|
||||||
invoice rendering matches what the user actually
|
// and we're not editing, render the saved
|
||||||
entered. For company: include it as the first
|
// org_billing record (the authoritative source)
|
||||||
line. */}
|
// rather than config.billingAddress, which is the
|
||||||
{!isPersonal &&
|
// wizard's empty default state because the billing
|
||||||
config.billingAddress.company &&
|
// step was skipped. In edit mode, fall back to
|
||||||
config.billingAddress.company.trim().length > 0 && (
|
// config.billingAddress, which is pre-populated
|
||||||
<div>{config.billingAddress.company}</div>
|
// from the request being edited.
|
||||||
)}
|
const useSaved =
|
||||||
<div>{config.billingAddress.street}</div>
|
hasOrgBilling && !isEditing && existingOrgBilling;
|
||||||
<div>
|
const company = useSaved
|
||||||
{config.billingAddress.postalCode}{" "}
|
? existingOrgBilling!.companyName
|
||||||
{config.billingAddress.city}
|
: config.billingAddress.company;
|
||||||
</div>
|
const street = useSaved
|
||||||
<div className="text-text-muted">
|
? existingOrgBilling!.streetAddress
|
||||||
{tCountries(
|
: config.billingAddress.street;
|
||||||
config.billingAddress.country as SupportedCountry
|
const postalCode = useSaved
|
||||||
)}
|
? existingOrgBilling!.postalCode
|
||||||
</div>
|
: config.billingAddress.postalCode;
|
||||||
</div>
|
const city = useSaved
|
||||||
|
? existingOrgBilling!.city
|
||||||
|
: config.billingAddress.city;
|
||||||
|
const country = useSaved
|
||||||
|
? existingOrgBilling!.country
|
||||||
|
: config.billingAddress.country;
|
||||||
|
const contactName = useSaved
|
||||||
|
? existingOrgBilling!.contactName
|
||||||
|
: null;
|
||||||
|
return (
|
||||||
|
<div className="text-text-primary text-right">
|
||||||
|
{/* For personal: skip the company line so the
|
||||||
|
invoice rendering matches what the user actually
|
||||||
|
entered. For company: include it as the first
|
||||||
|
line. */}
|
||||||
|
{!isPersonal &&
|
||||||
|
company &&
|
||||||
|
company.trim().length > 0 && <div>{company}</div>}
|
||||||
|
{/* Phase 6 fix2: optional contact-person line
|
||||||
|
("z.Hd. <name>") only present when the saved
|
||||||
|
org_billing has it set. */}
|
||||||
|
{contactName && contactName.trim().length > 0 && (
|
||||||
|
<div className="text-text-muted">
|
||||||
|
{t("reviewContactPersonPrefix")} {contactName}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>{street}</div>
|
||||||
|
<div>
|
||||||
|
{postalCode} {city}
|
||||||
|
</div>
|
||||||
|
<div className="text-text-muted">
|
||||||
|
{tCountries(country as SupportedCountry)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{/* Bug 35: VAT review row. Company customers see this so
|
{/* Bug 35: VAT review row. Company customers see this so
|
||||||
they can verify the VAT id they typed before submitting.
|
they can verify the VAT id they typed before submitting.
|
||||||
Personal customers never see it — they don't have a
|
Personal customers never see it — they don't have a
|
||||||
VAT number, the form didn't ask, the review hides it. */}
|
VAT number, the form didn't ask, the review hides it.
|
||||||
|
Phase 6 fix3: when reading from existingOrgBilling,
|
||||||
|
the value comes from there too. */}
|
||||||
{!isPersonal &&
|
{!isPersonal &&
|
||||||
config.billingAddress.vatNumber &&
|
(() => {
|
||||||
config.billingAddress.vatNumber.trim().length > 0 && (
|
const vat =
|
||||||
<ReviewRow
|
hasOrgBilling && !isEditing && existingOrgBilling
|
||||||
label={t("billingVatNumber")}
|
? existingOrgBilling.vatNumber
|
||||||
value={config.billingAddress.vatNumber}
|
: config.billingAddress.vatNumber;
|
||||||
mono
|
return vat && vat.trim().length > 0 ? (
|
||||||
/>
|
<ReviewRow
|
||||||
)}
|
label={t("billingVatNumber")}
|
||||||
|
value={vat}
|
||||||
|
mono
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
})()}
|
||||||
<ReviewRow
|
<ReviewRow
|
||||||
label={t("reviewContactEmail")}
|
label={t("reviewContactEmail")}
|
||||||
value={userEmail || ""}
|
value={userEmail || ""}
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"saveChanges": "Änderungen speichern",
|
"saveChanges": "Änderungen speichern",
|
||||||
"billingVatNumber": "MWST-Nummer",
|
"billingVatNumber": "MWST-Nummer",
|
||||||
"billingVatHelp": "Ihre registrierte MWST-Nummer. Falls Ihre Firma von der MWST befreit ist, leer lassen und in den Notizen erläutern.",
|
"billingVatHelp": "Ihre registrierte MWST-Nummer. Falls Ihre Firma von der MWST befreit ist, leer lassen und in den Notizen erläutern.",
|
||||||
"billingNotesPlaceholderPersonal": "Was wir wissen sollten — bevorzugte Zahlungsart, Rechnungsreferenz, etc."
|
"billingNotesPlaceholderPersonal": "Was wir wissen sollten — bevorzugte Zahlungsart, Rechnungsreferenz, etc.",
|
||||||
|
"reviewContactPersonPrefix": "z.Hd."
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"saveChanges": "Save changes",
|
"saveChanges": "Save changes",
|
||||||
"billingVatNumber": "VAT number",
|
"billingVatNumber": "VAT number",
|
||||||
"billingVatHelp": "Your registered VAT identifier. If your company is VAT-exempt, leave blank and explain in the notes field.",
|
"billingVatHelp": "Your registered VAT identifier. If your company is VAT-exempt, leave blank and explain in the notes field.",
|
||||||
"billingNotesPlaceholderPersonal": "Anything we should know — preferred payment method, billing reference, etc."
|
"billingNotesPlaceholderPersonal": "Anything we should know — preferred payment method, billing reference, etc.",
|
||||||
|
"reviewContactPersonPrefix": "Attn:"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"saveChanges": "Enregistrer les modifications",
|
"saveChanges": "Enregistrer les modifications",
|
||||||
"billingVatNumber": "Numéro de TVA",
|
"billingVatNumber": "Numéro de TVA",
|
||||||
"billingVatHelp": "Votre identifiant TVA enregistré. Si votre entreprise est exonérée de TVA, laissez vide et précisez dans les notes.",
|
"billingVatHelp": "Votre identifiant TVA enregistré. Si votre entreprise est exonérée de TVA, laissez vide et précisez dans les notes.",
|
||||||
"billingNotesPlaceholderPersonal": "Tout ce que nous devons savoir — moyen de paiement préféré, référence de facturation, etc."
|
"billingNotesPlaceholderPersonal": "Tout ce que nous devons savoir — moyen de paiement préféré, référence de facturation, etc.",
|
||||||
|
"reviewContactPersonPrefix": "À l'attention de"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Tableau de bord",
|
"title": "Tableau de bord",
|
||||||
|
|||||||
@@ -121,7 +121,8 @@
|
|||||||
"saveChanges": "Salva modifiche",
|
"saveChanges": "Salva modifiche",
|
||||||
"billingVatNumber": "Partita IVA",
|
"billingVatNumber": "Partita IVA",
|
||||||
"billingVatHelp": "Il tuo identificativo IVA registrato. Se la tua azienda è esente IVA, lascia vuoto e spiega nelle note.",
|
"billingVatHelp": "Il tuo identificativo IVA registrato. Se la tua azienda è esente IVA, lascia vuoto e spiega nelle note.",
|
||||||
"billingNotesPlaceholderPersonal": "Qualsiasi cosa dovremmo sapere — metodo di pagamento preferito, riferimento per fatturazione, ecc."
|
"billingNotesPlaceholderPersonal": "Qualsiasi cosa dovremmo sapere — metodo di pagamento preferito, riferimento per fatturazione, ecc.",
|
||||||
|
"reviewContactPersonPrefix": "c.a."
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Dashboard",
|
||||||
|
|||||||
Reference in New Issue
Block a user