diff --git a/studio/schema.ts b/studio/schema.ts index 043f5c69e..22220e0c0 100644 --- a/studio/schema.ts +++ b/studio/schema.ts @@ -12,7 +12,7 @@ import posts from "./schemas/documents/post"; import categories from "./schemas/fields/categories"; import legalDocument from "./schemas/documents/legalDocuments"; import benefit from "./schemas/documents/benefit"; -import location from "./schemas/documents/location"; +import companyLocations from "./schemas/documents/companyLocations"; import compensations from "./schemas/documents/compensations"; import salaryAndBenefits from "./schemas/documents/salaryAndBenefits"; import siteSettings from "./schemas/documents/siteSettings"; @@ -35,6 +35,6 @@ export const schema: { types: SchemaTypeDefinition[] } = { compensations, salaryAndBenefits, benefit, - location, + companyLocations, ], }; diff --git a/studio/schemas/deskStructure.ts b/studio/schemas/deskStructure.ts index 1bd7443dc..bf287431f 100644 --- a/studio/schemas/deskStructure.ts +++ b/studio/schemas/deskStructure.ts @@ -17,6 +17,7 @@ import { soMeLinksID } from "./documents/socialMediaProfiles"; import { companyInfoID } from "./documents/companyInfo"; import { legalDocumentID } from "./documents/legalDocuments"; import { compensationsId } from "./documents/compensations"; +import { companyLocationsID } from "./documents/companyLocations"; export default (S: StructureBuilder) => S.list() @@ -36,13 +37,17 @@ export default (S: StructureBuilder) => S.document() .schemaType(companyInfoID) .documentId(companyInfoID) - .title("Company Information"), + .title("Company Information") ), S.listItem() - .title("Locations") + .title("Company Locations") .icon(PinIcon) - .child(S.documentTypeList("location").title("Locations")), - ]), + .child( + S.documentTypeList(companyLocationsID).title( + "Company Locations" + ) + ), + ]) ), S.listItem() .title("Legal Documents") @@ -59,7 +64,7 @@ export default (S: StructureBuilder) => S.document() .schemaType("navigationManager") .documentId("navigationManager") - .title("Navigation Manager"), + .title("Navigation Manager") ), S.listItem() .title("Dynamic Pages") @@ -79,14 +84,17 @@ export default (S: StructureBuilder) => S.document() .schemaType(blogId) .documentId(blogId) - .title("Blog Overview & Settings"), + .title("Blog Overview & Settings") ), S.listItem() .title("Compensations") .icon(HeartIcon) .child( - S.documentTypeList(compensationsId).title("Compensations"), + S.document() + .schemaType(compensationsId) + .documentId(compensationsId) + .title("Compensations") ), - ]), + ]) ), ]); diff --git a/studio/schemas/documents/companyLocations.ts b/studio/schemas/documents/companyLocations.ts new file mode 100644 index 000000000..3b11ac541 --- /dev/null +++ b/studio/schemas/documents/companyLocations.ts @@ -0,0 +1,20 @@ +import { defineField } from "sanity"; + +export const companyLocationsID = "companyLocations"; +export const companyLocationID = "companyLocation"; + +const companyLocations = defineField({ + name: companyLocationsID, + type: "document", + title: "Location", + description: "Content related to an individual location within the company", + fields: [ + defineField({ + name: companyLocationID, + type: "string", + title: "Location", + }), + ], +}); + +export default companyLocations; diff --git a/studio/schemas/documents/compensations.ts b/studio/schemas/documents/compensations.ts index 70f316a8b..8d85c73c4 100644 --- a/studio/schemas/documents/compensations.ts +++ b/studio/schemas/documents/compensations.ts @@ -1,13 +1,9 @@ import { defineField, defineType } from "sanity"; import { titleSlug } from "../schemaTypes/slug"; import seo from "../objects/seo"; -import locations from "../objects/locations"; import { title } from "../fields/text"; import { benefitId } from "./benefit"; -import { compensationDetails } from "../objects/compensationData"; - -// maximum number of locations to display in the preview without truncating -const LOCATIONS_PREVIEW_CUTOFF = 3; +import { bonusesByLocation } from "../objects/compensations/bonusesByLocation"; export const compensationsId = "compensations"; @@ -16,10 +12,39 @@ const compensations = defineType({ type: "document", title: "Compensations", fields: [ - title, + { + ...title, + title: "Compensation Page Title", + description: + "Enter the primary title that will be displayed at the top of the compensation page. This is what users will see when they visit the page.", + }, titleSlug, - locations, - compensationDetails, + // add pension here. pension doesn't rely on locations + bonusesByLocation, + // add salary here + // benefits should be updated to match the grouping by location: + // defineField({ + // name: "benefits", + // title: "Benefits", + // type: "array", + // of: [ + // defineField({ + // name: "benefitGroup", + // title: "Benefit Group", + // type: "object", + // fields: [ + // locations, + // defineField({ + // name: "benefitsList", + // title: "List of Benefits", + // type: "array", + // of: [{ type: benefitId }], + // }), + // ], + // }), + // ], + // }), + // IMPORTANT: this is just a very simple mockup and might not represent a good ux defineField({ name: "benefits", title: "Included Benefits", @@ -32,53 +57,14 @@ const compensations = defineType({ ], preview: { select: { - title: "basicTitle", - /* - Access array object values using dot notation with the array index, e.g., "locations.0.basicTitle". - This approach allows selecting a subset of the array for preview purposes. - For more details, see: https://www.sanity.io/docs/previews-list-views#62febb15a63a - */ - ...[...Array(LOCATIONS_PREVIEW_CUTOFF + 1).keys()].reduce( - (o, i) => ({ ...o, [`location${i}`]: `locations.${i}.basicTitle` }), - {}, - ), + title: "title", }, - prepare({ title, ...locationsMap }) { + prepare({ title }) { return { title, - subtitle: previewStringFromLocationsMap( - locationsMap, - LOCATIONS_PREVIEW_CUTOFF, - ), }; }, }, }); -/** - * Generates a preview string based on the selected office locations. - * - * @param {Object} locationsMap - A map of office titles selected for preview, e.g., { office0: "Trondheim", office1: "Oslo", ... }. - * @param {number} cutoff - The maximum number of locations to display before shortening the preview string. - * @returns {string|undefined} - A formatted string summarizing the selected locations or `undefined` if no locations are selected. - */ -function previewStringFromLocationsMap( - locationsMap: { - [key: string]: string; - }, - cutoff: number, -): string | undefined { - const locations = Object.values(locationsMap).filter( - (o) => o !== undefined, - ); - if (locations.length === 0) { - return undefined; - } else if (locations.length === 1) { - return `Location: ${locations[0]}`; - } else if (locations.length > cutoff) { - return `Locations: ${locations.toSpliced(cutoff - 1).join(", ")}, and more`; - } - return `Locations: ${locations.toSpliced(-1).join(", ")} and ${locations.at(-1)}`; -} - export default compensations; diff --git a/studio/schemas/documents/location.ts b/studio/schemas/documents/location.ts deleted file mode 100644 index 9c5970dc2..000000000 --- a/studio/schemas/documents/location.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineType } from "sanity"; -import { title } from "../fields/text"; - -export const locationId = "location"; - -const location = defineType({ - name: locationId, - type: "document", - title: "Location", - description: "Content related to an individual location within the company", - fields: [title], -}); - -export default location; diff --git a/studio/schemas/objects/compensationData.ts b/studio/schemas/objects/compensationData.ts deleted file mode 100644 index 5075545c1..000000000 --- a/studio/schemas/objects/compensationData.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineField } from "sanity"; - -export const compensationDetailsID = "compensationDetails"; - -export const compensationDetails = defineField({ - name: compensationDetailsID, - title: "Compensation Details", - description: - "Add and manage detailed information on compensation elements like estimated salary, bonuses, pensions, and other financial benefits.", - type: "object", - options: { - collapsible: true, - collapsed: false, - }, - fields: [ - { - name: "showEstimatedSalary", - title: "Show Estimated Salary", - type: "boolean", - description: - "Toggle this option to show or hide the estimated salary for the locations you've selected. Disable this if the feature isn't ready or if you prefer not to display salary details at this time.", - initialValue: false, - }, - { - name: "replaceMe", - title: "Replace me", - type: "string", - readOnly: ({ parent }) => !parent?.showEstimatedSalary, - }, - ], - preview: { - select: { - title: "basicTitle", - }, - prepare(selection) { - return { - title: selection.title, - }; - }, - }, -}); diff --git a/studio/schemas/objects/compensations/bonusesByLocation.ts b/studio/schemas/objects/compensations/bonusesByLocation.ts new file mode 100644 index 000000000..06d9bb411 --- /dev/null +++ b/studio/schemas/objects/compensations/bonusesByLocation.ts @@ -0,0 +1,97 @@ +import { defineField } from "sanity"; +import { location, locationID } from "../locations"; +import { companyLocationID } from "studio/schemas/documents/companyLocations"; + +export const bonusesByLocation = defineField({ + name: "bonusesByLocation", + title: "Bonus by Location", + description: + "Enter the average bonus amount for each company location. Each location can have only one bonus entry, but you can assign the same bonus amount to multiple locations.", + type: "array", + of: [ + defineField({ + name: "bonusData", + title: "Bonus Information", + description: + "Details of the bonus amount specific to a particular location. Each location should have a unique entry with the average bonus calculated for that site.", + type: "object", + fields: [ + { + ...location, + description: + "Select the company location for which you are entering the average bonus information. Each location must be unique.", + validation: (Rule) => Rule.required(), + }, + defineField({ + name: "averageBonus", + title: "Average Bonus", + description: + "Enter the average bonus amount for this location. Ensure the amount is positive and reflective of the compensation package for this location.", + type: "number", + validation: (Rule) => + Rule.required() + .min(0) + .error("Please enter a positive bonus amount."), + }), + ], + preview: { + select: { + averageBonus: "averageBonus", + location: `${locationID}.${companyLocationID}`, + }, + prepare({ averageBonus, location }) { + return { + title: `Average Bonus: ${averageBonus || "N/A"}`, + subtitle: `Location: ${location || "No location selected"}`, + }; + }, + }, + }), + ], + validation: (Rule) => + Rule.custom((bonusesByLocation, context) => { + const duplicateCheck = checkForDuplicateLocations( + bonusesByLocation as BonusEntry[] | undefined + ); + + if (duplicateCheck !== true) return duplicateCheck; + + return true; + }), +}); + +interface LocationReference { + _ref: string; + _type: string; + title?: string; +} + +interface BonusEntry { + location: LocationReference; + averageBonus: number; +} + +/** + * Checks for duplicate location references in the bonusesByLocation array. + * Ensures each location has a unique bonus entry. + * + * @param {BonusEntry[] | undefined} bonusesByLocation - The array of bonus entries, each with one or more locations. + * @returns {string | true} - Returns an error message if duplicate locations are found, or true if all are unique. + */ +const checkForDuplicateLocations = ( + bonusesByLocation: BonusEntry[] | undefined +): string | true => { + if (!bonusesByLocation) return true; + + const locationRefs = bonusesByLocation + .map((entry) => entry.location?._ref) + .filter(Boolean); + + const uniqueRefs = new Set(locationRefs); + + if (uniqueRefs.size !== locationRefs.length) { + return "Each location should be listed only once in the bonuses list. You can assign the same bonus amount to multiple locations, but make sure no location appears more than once."; + } + + return true; +}; diff --git a/studio/schemas/objects/locations.ts b/studio/schemas/objects/locations.ts index 36673297f..ed889e922 100644 --- a/studio/schemas/objects/locations.ts +++ b/studio/schemas/objects/locations.ts @@ -1,22 +1,26 @@ import { defineField } from "sanity"; -import { locationId } from "../documents/location"; +import { companyLocationsID } from "../documents/companyLocations"; -const locations = defineField({ - name: "locations", +export const locationsID = "locations"; +export const locationID = "location"; + +export const location = defineField({ + name: locationID, + type: "reference", + title: "Select a location", + description: + "Select the office location this content applies to. If it applies to all locations, you can leave this field empty.", + to: [{ type: companyLocationsID }], + options: { + disableNew: true, + }, +}); + +export const locations = defineField({ + name: locationsID, type: "array", title: "Relevant Locations", description: "You can tailor this content to specific office locations by selecting them here. If the content applies to all locations, just leave this field empty.", - of: [ - { - title: "Select a location", - type: "reference", - to: [{ type: locationId }], - options: { - disableNew: true, - }, - }, - ], + of: [location], }); - -export default locations; diff --git a/studio/schemas/objects/seo.ts b/studio/schemas/objects/seo.ts index af7189e7f..ffceb4876 100644 --- a/studio/schemas/objects/seo.ts +++ b/studio/schemas/objects/seo.ts @@ -37,10 +37,10 @@ const seo = defineField({ "An optional but recommended short description to boost visitor engagement from social media and search engines. Try to keep it between 70-160 characters.", validation: (Rule) => [ Rule.min(70).warning( - "A description of at least 70 characters has a higher chance of converting visitors", + "A description of at least 70 characters has a higher chance of converting visitors" ), Rule.max(160).warning( - "A description of more than 160 characters has a lower chance of converting visitors", + "A description of more than 160 characters has a lower chance of converting visitors" ), ], }),