From b5a1a8eb6b6b47eb3e7337362fc38f44926011c6 Mon Sep 17 00:00:00 2001 From: Jens Gryspeert Date: Tue, 2 Jun 2026 14:08:31 +0200 Subject: [PATCH 1/3] feat(uniformbuilder): add award/MOS constants, enums and catalog Introduce a constants/ module as the single source of truth for award types, ribbon attachment types (with their max-count map), MOS codes and groupings, and reused award-name fragments. Add an ordered AWARD_CATALOG data table plus hasValorDevice/stripValorDevice helpers. Unused until the consumers adopt it in the following commit. --- .../modules/constants/awardAttachmentTypes.js | 28 + .../modules/constants/awardCatalog.js | 613 ++++++++++++++++++ .../modules/constants/awardNames.js | 18 + .../modules/constants/awardTypes.js | 14 + .../uniformbuilder/modules/constants/index.js | 9 + .../uniformbuilder/modules/constants/mos.js | 63 ++ 6 files changed, 745 insertions(+) create mode 100644 client/app/uniformbuilder/modules/constants/awardAttachmentTypes.js create mode 100644 client/app/uniformbuilder/modules/constants/awardCatalog.js create mode 100644 client/app/uniformbuilder/modules/constants/awardNames.js create mode 100644 client/app/uniformbuilder/modules/constants/awardTypes.js create mode 100644 client/app/uniformbuilder/modules/constants/index.js create mode 100644 client/app/uniformbuilder/modules/constants/mos.js diff --git a/client/app/uniformbuilder/modules/constants/awardAttachmentTypes.js b/client/app/uniformbuilder/modules/constants/awardAttachmentTypes.js new file mode 100644 index 0000000..1221311 --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/awardAttachmentTypes.js @@ -0,0 +1,28 @@ +// Ribbon/medal attachment-device taxonomy and the max displayable count per +// device. MAX_AWARD_COUNT mirrors the AwardRegistry.getMaxAwardCount switch — +// keep the numbers in sync here, not scattered across files. +export const AwardAttachmentType = Object.freeze({ + OAK_CLUSTERS: "oakClusters", + UNIT_CITATION_CLUSTERS: "unitCitationClusters", + UNIT_CITATION_S_STARS: "unitCitationSStars", + OAK_CLUSTERS_SERVICE: "oakClustersService", + OAK_CLUSTERS_VALOR: "oakClustersValor", + SILVER_STARS: "silverStars", + STARS: "stars", + STARS_DONATION: "starsDonation", + GC_NOTCHES: "gcNotches", + NCO_NUMS: "ncoNums", +}); + +export const MAX_AWARD_COUNT = Object.freeze({ + [AwardAttachmentType.OAK_CLUSTERS]: 19, + [AwardAttachmentType.UNIT_CITATION_CLUSTERS]: 10, + [AwardAttachmentType.UNIT_CITATION_S_STARS]: 5, + [AwardAttachmentType.OAK_CLUSTERS_SERVICE]: 6, + [AwardAttachmentType.OAK_CLUSTERS_VALOR]: 14, + [AwardAttachmentType.SILVER_STARS]: 5, + [AwardAttachmentType.STARS]: 10, + [AwardAttachmentType.STARS_DONATION]: 12, + [AwardAttachmentType.GC_NOTCHES]: 9, + [AwardAttachmentType.NCO_NUMS]: 6, +}); diff --git a/client/app/uniformbuilder/modules/constants/awardCatalog.js b/client/app/uniformbuilder/modules/constants/awardCatalog.js new file mode 100644 index 0000000..81e38e7 --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/awardCatalog.js @@ -0,0 +1,613 @@ +import { AwardType } from "./awardTypes"; +import { AwardAttachmentType } from "./awardAttachmentTypes"; + +// The full award catalog: name + per-award metadata. AwardRegistry loops this +// in order to populate its Map, so KEEP THE ORDER STABLE — Map iteration order +// is insertion order and downstream code may rely on it. `name` is the Map key; +// every other field becomes the stored detail object verbatim. +export const AWARD_CATALOG = [ + //____ MAINLINE MEDALS AND RIBBONS ____ + { + name: `7th Cavalry Lifetime Dedication Award`, + awardPriority: 0, + medalPriority: 0, + awardType: AwardType.Medal, + }, + { + name: `James "Krazee" Foster Lifetime Achievement Medal`, + awardPriority: 1, + medalPriority: 1, + awardType: AwardType.Medal, + }, + { + name: `Ronnie "Coldblud" Bussey Lifetime Achievement Medal`, + awardPriority: 2, + medalPriority: 2, + awardType: AwardType.Medal, + }, + { + name: "Army Distinguished Service Cross", + awardPriority: 3, + medalPriority: 3, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Defense Distinguished Service Medal", + awardPriority: 4, + medalPriority: 4, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Army Distinguished Service Medal", + awardPriority: 5, + medalPriority: 5, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Silver Star", + awardPriority: 6, + medalPriority: 6, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Defense Superior Service Medal", + awardPriority: 7, + medalPriority: 7, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Legion of Merit", + awardPriority: 8, + medalPriority: 8, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Distinguished Flying Cross", + awardPriority: 9, + medalPriority: 9, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Soldiers Medal", + awardPriority: 10, + medalPriority: 10, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Bronze Star", + awardPriority: 11, + medalPriority: 11, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.MedalWithValor, + }, + { + name: "Purple Heart", + awardPriority: 12, + medalPriority: 12, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Defense Meritorious Service Medal", + awardPriority: 13, + medalPriority: 13, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Meritorious Service Medal", + awardPriority: 14, + medalPriority: 14, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Air Medal", + awardPriority: 15, + medalPriority: 15, + awardAttachmentType: AwardAttachmentType.NCO_NUMS, + awardType: AwardType.Medal, + }, + { + name: "Joint Service Commendation Medal", + awardPriority: 16, + medalPriority: 16, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Army Commendation Medal", + awardPriority: 17, + medalPriority: 17, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.MedalWithValor, + }, + { + name: "Joint Service Achievement Medal", + awardPriority: 18, + medalPriority: 18, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Army Achievement Medal", + awardPriority: 19, + medalPriority: 19, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Prisoner of War Medal", + awardPriority: 20, + medalPriority: 20, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + { + name: "Army Good Conduct Medal", + awardPriority: 21, + medalPriority: 21, + awardAttachmentType: AwardAttachmentType.GC_NOTCHES, + awardType: AwardType.Medal, + }, + { + name: "Armed Forces Expeditionary Medal", + awardPriority: 22, + medalPriority: 22, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Afghanistan Campaign Medal", + awardPriority: 23, + medalPriority: 23, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Iraq Campaign Medal", + awardPriority: 24, + medalPriority: 24, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Global War on Terrorism Expeditionary Medal", + awardPriority: 25, + medalPriority: 25, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "National Defense Service Medal", + awardPriority: 26, + medalPriority: 26, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Armed Forces Service Medal", + awardPriority: 27, + medalPriority: 27, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Humanitarian Service Medal", + awardPriority: 28, + medalPriority: 28, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Donation Ribbon", + awardPriority: 29, + awardAttachmentType: AwardAttachmentType.STARS_DONATION, + awardType: AwardType.RibbonDonationLogic, + }, // Requires Special Case + { + name: "7th Cavalry Server Upgrade Award", + awardPriority: 30, + medalPriority: 29, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.MedalTiered, + }, // Fuck you, whoever put this into SOP + { + name: "StackUp Donation Medal", + awardPriority: 31, + medalPriority: 30, + awardAttachmentType: AwardAttachmentType.GC_NOTCHES, + awardType: AwardType.MedalTiered, + }, // and again + { + name: "Outstanding Volunteer Service Medal", + awardPriority: 32, + medalPriority: 31, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "NCO Professional Development Ribbon", + awardPriority: 33, + awardAttachmentType: AwardAttachmentType.NCO_NUMS, + awardType: AwardType.Ribbon, + }, + { + name: "Honor Graduate Ribbon", + awardPriority: 34, + awardType: AwardType.Ribbon, + }, + { + name: "Army Service Ribbon", + awardPriority: 35, + awardType: AwardType.Ribbon, + }, + { + name: "Cavalry Centurion Medal", + awardPriority: 36, + medalPriority: 32, + awardAttachmentType: AwardAttachmentType.SILVER_STARS, + awardType: AwardType.Medal, + }, + { + name: "United Nations Service Medal", + awardPriority: 37, + medalPriority: 33, + awardAttachmentType: AwardAttachmentType.STARS, + awardType: AwardType.Medal, + }, + { + name: "Overseas Service Ribbon", + awardPriority: 38, + medalPriority: 34, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Ready or Not Service Ribbon", + awardPriority: 39, + medalPriority: 35, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "DCS World Service Ribbon", + awardPriority: 40, + medalPriority: 36, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Squad Service Ribbon", + awardPriority: 41, + medalPriority: 37, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "WWII Service Ribbon", + awardPriority: 42, + medalPriority: 38, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Hell Let Loose Service Ribbon", + awardPriority: 43, + medalPriority: 39, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Hell Let Loose Console Service Ribbon", + awardPriority: 44, + medalPriority: 40, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Battlefield 6 Service Ribbon", + awardPriority: 45, + medalPriority: 41, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS_SERVICE, + awardType: AwardType.Medal, + }, + { + name: "Recruiting Ribbon", + awardPriority: 46, + awardAttachmentType: AwardAttachmentType.STARS_DONATION, + awardType: AwardType.RibbonDonationLogic, + }, // May Also require Special Case + { + name: "D-Day Commemorative Medal", + awardPriority: 47, + medalPriority: 42, + awardType: AwardType.Medal, + }, + { + name: "Ranger Selection Ribbon", + awardPriority: 48, + awardType: AwardType.Ribbon, + }, + { + name: "Sniper Ribbon", + awardPriority: 49, + medalPriority: 43, + awardType: AwardType.Medal, + }, + { + name: "Basic Assault Course Ribbon", + awardPriority: 50, + awardType: AwardType.Ribbon, + }, + + // ___ DISCONTINUED RIBBONS/MEDALS WITHOUT PRECIDENCE ___ + // These ones are a bit of an unknown precidence wise. Indeed we have some discon awards above, however precidence is known + // Anything here is shown as is, and there is no inherent precicence for these. + { + name: "Cadre Course Ribbon", + awardPriority: 51, + awardType: AwardType.Ribbon, + }, + { + name: "Womens Army Corp Service Medal", + awardPriority: 52, + medalPriority: 44, + awardType: AwardType.Medal, + }, + { + name: "D Day Participation Ribbon", + awardPriority: 53, + awardType: AwardType.Ribbon, + }, + { + name: "European/African/Middle Eastern Campaign Medal", + awardPriority: 54, + medalPriority: 45, + awardAttachmentType: AwardAttachmentType.OAK_CLUSTERS, + awardType: AwardType.Medal, + }, + + //____ UNIT CITATIONS ____ + { + name: "Army & Air Force Presidential Unit Citation", + awardPriority: 0, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_CLUSTERS, + awardType: AwardType.UnitCitation, + }, + { + name: "Army Valorous Unit Citation", + awardPriority: 1, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_CLUSTERS, + awardType: AwardType.UnitCitation, + }, + { + name: "Joint Meritorious Unit Citation", + awardPriority: 2, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_CLUSTERS, + awardType: AwardType.UnitCitation, + }, + { + name: "Army Meritorious Unit Citation", + awardPriority: 3, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_CLUSTERS, + awardType: AwardType.UnitCitation, + }, + { + name: "Army Superior Unit Citation", + awardPriority: 4, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_CLUSTERS, + awardType: AwardType.UnitCitation, + }, + { + name: "7th Cavalry Black Ops Unit Citation", + awardPriority: 5, + awardAttachmentType: AwardAttachmentType.UNIT_CITATION_S_STARS, + awardType: AwardType.UnitCitation, + }, + + // ____ COMBAT BADGES ____ + { + name: "Flight Medic Badge", + awardPriority: 6, + awardType: AwardType.BadgeCombat, + }, // (3/1/b/1-7) (4/1/b/1-7) + { + name: "Master Army Aviator Badge", + awardPriority: 8, + awardType: AwardType.BadgeCombat, + }, // (A/1-7) (A/ACD) + { + name: "Senior Army Aviator Badge", + awardPriority: 7, + awardType: AwardType.BadgeCombat, + }, + { + name: "Army Aviator Badge", + awardPriority: 6, + awardType: AwardType.BadgeCombat, + }, + { + name: "Aircraft Master Crewman Badge", + awardPriority: 8, + awardType: AwardType.BadgeCombat, + }, // (A/1-7) (A/ACD) + { + name: "Aircraft Senior Crewman Badge", + awardPriority: 7, + awardType: AwardType.BadgeCombat, + }, + { + name: "Aircraft Crewman Badge", + awardPriority: 6, + awardType: AwardType.BadgeCombat, + }, + { + name: "Combat Infantry Badge 4th Award", + awardPriority: 5, + awardType: AwardType.BadgeCombat, + }, + { + name: "Combat Infantry Badge 2nd Award", + awardPriority: 3, + awardType: AwardType.BadgeCombat, + }, + { + name: "Combat Infantry Badge 3rd Award", + awardPriority: 4, + awardType: AwardType.BadgeCombat, + }, + { + name: "Combat Infantry Badge", + awardPriority: 2, + awardType: AwardType.BadgeCombat, + }, + { + name: "Expert Infantry Badge", + awardPriority: 1, + awardType: AwardType.BadgeCombat, + }, // et. al. + + //____ WEAPON QUALS ____ + { name: "Rifle Expert", awardTag: "rifle", awardType: AwardType.WeaponQual }, + { + name: "Rifle Sharpshooter", + awardTag: "rifle", + awardType: AwardType.WeaponQual, + }, + { + name: "Rifle Marksman", + awardTag: "rifle", + awardType: AwardType.WeaponQual, + }, + { + name: "Grenade Expert", + awardTag: "grenade", + awardType: AwardType.WeaponQual, + }, + { + name: "Grenade Sharpshooter", + awardTag: "grenade", + awardType: AwardType.WeaponQual, + }, + { + name: "Grenade Marksman", + awardTag: "grenade", + awardType: AwardType.WeaponQual, + }, + { + name: "Pistol Expert", + awardTag: "pistol", + awardType: AwardType.WeaponQual, + }, + { + name: "Pistol Sharpshooter", + awardTag: "pistol", + awardType: AwardType.WeaponQual, + }, + { + name: "Pistol Marksman", + awardTag: "pistol", + awardType: AwardType.WeaponQual, + }, + { name: "M-203 Expert", awardTag: "m203", awardType: AwardType.WeaponQual }, + { + name: "M-203 Sharpshooter", + awardTag: "m203", + awardType: AwardType.WeaponQual, + }, + { name: "M-203 Marksman", awardTag: "m203", awardType: AwardType.WeaponQual }, + { + name: "Machine Gun Expert", + awardTag: "machineGun", + awardType: AwardType.WeaponQual, + }, + { + name: "Machine Gun Sharpshooter", + awardTag: "machineGun", + awardType: AwardType.WeaponQual, + }, + { + name: "Machine Gun Marksman", + awardTag: "machineGun", + awardType: AwardType.WeaponQual, + }, + { + name: "Recoilless Rifle Expert", + awardTag: "recoilless", + awardType: AwardType.WeaponQual, + }, + { + name: "Recoilless Rifle Sharpshooter", + awardTag: "recoilless", + awardType: AwardType.WeaponQual, + }, + { + name: "Recoilless Rifle Marksman", + awardTag: "recoilless", + awardType: AwardType.WeaponQual, + }, + { + name: "Aeroweapons Expert", + awardTag: "aeroweapons", + awardType: AwardType.WeaponQual, + }, + { + name: "Aeroweapons Sharpshooter", + awardTag: "aeroweapons", + awardType: AwardType.WeaponQual, + }, + { + name: "Aeroweapons Marksman", + awardTag: "aeroweapons", + awardType: AwardType.WeaponQual, + }, + { + name: "Hydra-70 Expert", + awardTag: "hydra70", + awardType: AwardType.WeaponQual, + }, + { + name: "Hydra-70 Sharpshooter", + awardTag: "hydra70", + awardType: AwardType.WeaponQual, + }, + { + name: "Hydra-70 Marksman", + awardTag: "hydra70", + awardType: AwardType.WeaponQual, + }, + { + name: "Tank Weapons Expert", + awardTag: "tankWeapons", + awardType: AwardType.WeaponQual, + }, + { + name: "Tank Weapons Sharpshooter", + awardTag: "tankWeapons", + awardType: AwardType.WeaponQual, + }, + { + name: "Tank Weapons Marksman", + awardTag: "tankWeapons", + awardType: AwardType.WeaponQual, + }, + + //____ TABS ____ + { name: "Special Forces Tab", awardPriority: 0, awardType: AwardType.Tab }, + { name: "Ranger Tab", awardPriority: 1, awardType: AwardType.Tab }, + { name: "Sapper Tab", awardPriority: 2, awardType: AwardType.Tab }, + { + name: "Long-Range Reconnaissance Patrol Tab", + awardPriority: 3, + awardType: AwardType.Tab, + }, +]; diff --git a/client/app/uniformbuilder/modules/constants/awardNames.js b/client/app/uniformbuilder/modules/constants/awardNames.js new file mode 100644 index 0000000..b953a6d --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/awardNames.js @@ -0,0 +1,18 @@ +// Award-name string fragments matched in logic (includes/replace/equality). +// Not the full award-name catalog — only the fragments duplicated across files. +export const AwardNameFragment = Object.freeze({ + // Matched bare via .includes(); the " " + form is the suffix stripped via .replace(). + VALOR_DEVICE: "with Valor Device", + FLIGHT_MEDIC_BADGE: "Flight Medic Badge", + AVIATOR: "Aviator", +}); + +// Does this award name carry the " with Valor Device" suffix? +export function hasValorDevice(awardName) { + return awardName.includes(AwardNameFragment.VALOR_DEVICE); +} + +// Strip the " with Valor Device" suffix, returning the base award name. +export function stripValorDevice(awardName) { + return awardName.replace(" " + AwardNameFragment.VALOR_DEVICE, ""); +} diff --git a/client/app/uniformbuilder/modules/constants/awardTypes.js b/client/app/uniformbuilder/modules/constants/awardTypes.js new file mode 100644 index 0000000..bd48474 --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/awardTypes.js @@ -0,0 +1,14 @@ +// Award type taxonomy. Values MUST equal the corresponding class names in +// AwardClasses.jsx — getCanvasObject routes on these strings to instantiate the +// matching class. Do not rename a value without renaming its class. +export const AwardType = Object.freeze({ + Medal: "Medal", + Ribbon: "Ribbon", + MedalWithValor: "MedalWithValor", + MedalTiered: "MedalTiered", + RibbonDonationLogic: "RibbonDonationLogic", + UnitCitation: "UnitCitation", + BadgeCombat: "BadgeCombat", + WeaponQual: "WeaponQual", + Tab: "Tab", +}); diff --git a/client/app/uniformbuilder/modules/constants/index.js b/client/app/uniformbuilder/modules/constants/index.js new file mode 100644 index 0000000..97258ed --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/index.js @@ -0,0 +1,9 @@ +export { AwardType } from "./awardTypes"; +export { AwardAttachmentType, MAX_AWARD_COUNT } from "./awardAttachmentTypes"; +export { Mos, MosGroup } from "./mos"; +export { + AwardNameFragment, + hasValorDevice, + stripValorDevice, +} from "./awardNames"; +export { AWARD_CATALOG } from "./awardCatalog"; diff --git a/client/app/uniformbuilder/modules/constants/mos.js b/client/app/uniformbuilder/modules/constants/mos.js new file mode 100644 index 0000000..00ce45e --- /dev/null +++ b/client/app/uniformbuilder/modules/constants/mos.js @@ -0,0 +1,63 @@ +// Military Occupational Specialty codes. Keys are the code prefixed with `M` +// (codes start with a digit and can't be bare identifiers); values are the +// exact API strings. Covers every code referenced in AwardClasses.jsx and +// GetUserInfo.jsx. +export const Mos = Object.freeze({ + M00Z: "00Z", + M01A: "01A", + M11A: "11A", + M11B: "11B", + M11C: "11C", + M12A: "12A", + M12B: "12B", + M13A: "13A", + M13B: "13B", + M15A: "15A", + M15T: "15T", + M153A: "153A", + M155A: "155A", + M155F: "155F", + M19A: "19A", + M19C: "19C", + M19D: "19D", + M19K: "19K", + M25A: "25A", + M25U: "25U", + M255N: "255N", + M26B: "26B", + M26Z: "26Z", + M27A: "27A", + M27D: "27D", + M31A: "31A", + M31B: "31B", + M35A: "35A", + M35B: "35B", + M42A: "42A", + M42B: "42B", + M46A: "46A", + M46S: "46S", + M47A: "47A", + M47C: "47C", + M47Q: "47Q", + M47T: "47T", + M49A: "49A", + M50A: "50A", + M51A: "51A", + M51S: "51S", + M57A: "57A", + M57B: "57B", + M67A: "67A", + M68W: "68W", + M79A: "79A", + M79R: "79R", + M79X: "79X", + M79Z: "79Z", +}); + +// Semantic groupings used by BadgeCombat badge logic in AwardClasses.jsx. +// Membership is the exact set the original if-chains tested — do not add codes. +export const MosGroup = Object.freeze({ + AVIATION: [Mos.M153A, Mos.M155A, Mos.M15A, Mos.M15T], + MEDICAL: [Mos.M68W, Mos.M67A], + AIRCREW: [Mos.M15T, Mos.M155F], +}); From 2ae44bec1b527136c4f186ae5a2f21f5ac9ff162 Mon Sep 17 00:00:00 2001 From: Jens Gryspeert Date: Tue, 2 Jun 2026 14:08:42 +0200 Subject: [PATCH 2/3] refactor(uniformbuilder): use constants, data-drive registry and simplify Replace duplicated award-type, attachment-type and MOS string literals across AwardRegistry, AwardClasses, getCanvasObject and GetUserInfo with references to the constants module. AwardRegistry now builds its Map by looping AWARD_CATALOG instead of ~100 imperative set() calls. Collapse the aviation/medical MOS if-chains to MosGroup membership checks and the MOS switches to enum cases. Route the valor-device strip through shared helpers and drop dead branches, redundant boolean comparisons and debug logging. MosGroup encodes the post-#129 aviation behavior: AVIATION includes 155F and AIRCREW is [15T] only, so 155F resolves to regular aviator wings. Behavior-preserving: enum values equal the original literals, catalog order and values match, and getMaxAwardCount returns the same numbers. --- .../uniformbuilder/modules/AwardClasses.jsx | 46 ++--- .../uniformbuilder/modules/AwardRegistry.jsx | 174 ++---------------- .../uniformbuilder/modules/GetUserInfo.jsx | 133 ++++++------- .../uniformbuilder/modules/constants/mos.js | 4 +- .../modules/getCanvasObject.jsx | 72 ++++---- 5 files changed, 140 insertions(+), 289 deletions(-) diff --git a/client/app/uniformbuilder/modules/AwardClasses.jsx b/client/app/uniformbuilder/modules/AwardClasses.jsx index 4efa386..c082107 100644 --- a/client/app/uniformbuilder/modules/AwardClasses.jsx +++ b/client/app/uniformbuilder/modules/AwardClasses.jsx @@ -1,3 +1,12 @@ +import { + AwardAttachmentType, + Mos, + MosGroup, + AwardNameFragment, + hasValorDevice, + stripValorDevice, +} from "./constants"; + export class Award { awardTitle = null; //awardDetail = null; @@ -19,7 +28,7 @@ export class Ribbon extends Award { const registryDetails = AwardRegistry.getAwardDetails(data.awardName); - if (!(registryDetails.awardAttachmentType == undefined)) { + if (registryDetails.awardAttachmentType != undefined) { this.ribbonAttachmentType = registryDetails.awardAttachmentType; } this.awardPriority = registryDetails.awardPriority; @@ -57,16 +66,16 @@ export class MedalWithValor extends Medal { constructor(data, AwardRegistry) { super(data, AwardRegistry); - if (this.awardTitle.includes("with Valor Device")) { + if (hasValorDevice(this.awardTitle)) { this.overrideAwardTitle(data.awardName); } } overrideAwardTitle(awardName) { - const baseAwardName = awardName.replace(" with Valor Device", ""); + const baseAwardName = stripValorDevice(awardName); this.awardTitle = baseAwardName; this.hasValorDevice = true; - this.ribbonAttachmentType = "oakClustersValor"; + this.ribbonAttachmentType = AwardAttachmentType.OAK_CLUSTERS_VALOR; this.maxAwardcount = 14; } } @@ -130,7 +139,7 @@ export class MedalTiered extends Medal { updateTieredMedal(detail) { //Stackup logic - if (this.ribbonAttachmentType == "gcNotches") { + if (this.ribbonAttachmentType == AwardAttachmentType.GC_NOTCHES) { switch (detail) { case "Gold Knot": this.highestTierAchieved = 3; @@ -157,7 +166,7 @@ export class MedalTiered extends Medal { } //Server upgrade ribbon logic - if (this.ribbonAttachmentType == "stars") { + if (this.ribbonAttachmentType == AwardAttachmentType.STARS) { switch (detail) { case "Gold Star": this.highestTierAchieved = 2; @@ -197,17 +206,11 @@ export class BadgeCombat extends Badge { this.awardPriority = registryDetails.awardPriority; this.userMos = userMos; - if ( - this.userMos == "153A" || - this.userMos == "155A" || - this.userMos == "15A" || - this.userMos == "15T" || - this.userMos == "155F" - ) { + if (MosGroup.AVIATION.includes(this.userMos)) { this.isAviation = true; } - if (this.userMos == "68W" || this.userMos == "67A") { + if (MosGroup.MEDICAL.includes(this.userMos)) { this.isMedical = true; } @@ -224,7 +227,7 @@ export class BadgeCombat extends Badge { //we need to give 15T an exception so that they stop at aircrew badges. if (this.isAviation) { - if (this.userMos == "15T") { + if (this.userMos == Mos.M15T) { this.maxAllowed = 8; } else { this.maxAllowed = 11; @@ -233,7 +236,6 @@ export class BadgeCombat extends Badge { } this.maxAllowed = 5; - return; } getImageNum(num) { @@ -278,15 +280,15 @@ export class BadgeCombat extends Badge { newAwardPriority <= this.maxAllowed ) { if ( - newAwardData.awardName == "Flight Medic Badge" && - this.isMedical == false + newAwardData.awardName == AwardNameFragment.FLIGHT_MEDIC_BADGE && + !this.isMedical ) { return; } if ( - newAwardData.awardName.includes("Aviator") && - this.isAviation == false + newAwardData.awardName.includes(AwardNameFragment.AVIATOR) && + !this.isAviation ) { return; } @@ -294,8 +296,6 @@ export class BadgeCombat extends Badge { this.awardTitle = newAwardData.awardName; this.awardPriority = newAwardPriority; this.imageNum = this.getImageNum(newAwardPriority); - } else { - return; } } } @@ -313,7 +313,7 @@ export class UnitCitation extends Award { const registryDetails = AwardRegistry.getAwardDetails(data.awardName); - if (!(registryDetails.awardAttachmentType == undefined)) { + if (registryDetails.awardAttachmentType != undefined) { this.ribbonAttachmentType = registryDetails.awardAttachmentType; } this.awardPriority = registryDetails.awardPriority; diff --git a/client/app/uniformbuilder/modules/AwardRegistry.jsx b/client/app/uniformbuilder/modules/AwardRegistry.jsx index c6969fd..a6aa759 100644 --- a/client/app/uniformbuilder/modules/AwardRegistry.jsx +++ b/client/app/uniformbuilder/modules/AwardRegistry.jsx @@ -1,137 +1,20 @@ +import { + MAX_AWARD_COUNT, + hasValorDevice, + stripValorDevice, + AWARD_CATALOG, +} from "./constants"; + export class AwardRegistry { constructor() { this.awards = new Map(); this.initalizeAwards(); } - // prettier-ignore initalizeAwards() { - - //____MAINLINE MEDALS AND RIBBONS____ - - this.awards.set(`7th Cavalry Lifetime Dedication Award`, {awardPriority: 0, medalPriority: 0, awardType: "Medal"}); - this.awards.set(`James "Krazee" Foster Lifetime Achievement Medal`, {awardPriority: 1, medalPriority: 1, awardType: "Medal"}); - this.awards.set(`Ronnie "Coldblud" Bussey Lifetime Achievement Medal`, {awardPriority: 2, medalPriority: 2, awardType: "Medal"}); - this.awards.set("Army Distinguished Service Cross", {awardPriority: 3, medalPriority: 3, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Defense Distinguished Service Medal", {awardPriority: 4, medalPriority: 4, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Army Distinguished Service Medal", {awardPriority: 5, medalPriority: 5, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Silver Star", {awardPriority: 6, medalPriority: 6, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Defense Superior Service Medal", {awardPriority: 7, medalPriority: 7, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Legion of Merit", {awardPriority: 8, medalPriority: 8, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Distinguished Flying Cross", {awardPriority: 9, medalPriority: 9, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Soldiers Medal", {awardPriority: 10, medalPriority: 10, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Bronze Star", {awardPriority: 11, medalPriority: 11, awardAttachmentType: "oakClusters", awardType: "MedalWithValor"}); - this.awards.set("Purple Heart", {awardPriority: 12, medalPriority: 12, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Defense Meritorious Service Medal", {awardPriority: 13, medalPriority: 13, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Meritorious Service Medal", {awardPriority: 14, medalPriority: 14, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Air Medal", {awardPriority: 15, medalPriority: 15, awardAttachmentType: "ncoNums", awardType: "Medal"}); - this.awards.set("Joint Service Commendation Medal", {awardPriority: 16, medalPriority: 16, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Army Commendation Medal", {awardPriority: 17, medalPriority: 17, awardAttachmentType: "oakClusters", awardType: "MedalWithValor"}); - this.awards.set("Joint Service Achievement Medal", {awardPriority: 18, medalPriority: 18, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Army Achievement Medal",{awardPriority: 19, medalPriority: 19, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Prisoner of War Medal", {awardPriority: 20, medalPriority: 20, awardAttachmentType: "oakClusters", awardType: "Medal"}); - this.awards.set("Army Good Conduct Medal", {awardPriority: 21, medalPriority: 21, awardAttachmentType: "gcNotches", awardType: "Medal"}); - this.awards.set("Armed Forces Expeditionary Medal", {awardPriority: 22, medalPriority: 22, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Afghanistan Campaign Medal", {awardPriority: 23, medalPriority: 23, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Iraq Campaign Medal", {awardPriority: 24, medalPriority: 24, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Global War on Terrorism Expeditionary Medal", {awardPriority: 25, medalPriority: 25, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("National Defense Service Medal", {awardPriority: 26, medalPriority: 26, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Armed Forces Service Medal", {awardPriority: 27, medalPriority: 27, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Humanitarian Service Medal", {awardPriority: 28, medalPriority: 28, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Donation Ribbon", {awardPriority: 29, awardAttachmentType: "starsDonation", awardType: "RibbonDonationLogic"}); // Requires Special Case - this.awards.set("7th Cavalry Server Upgrade Award", {awardPriority: 30, medalPriority: 29, awardAttachmentType: "stars", awardType: "MedalTiered"}); // Fuck you, whoever put this into SOP - this.awards.set("StackUp Donation Medal", {awardPriority: 31, medalPriority: 30, awardAttachmentType: "gcNotches", awardType: "MedalTiered"}); // and again - this.awards.set("Outstanding Volunteer Service Medal", {awardPriority: 32, medalPriority: 31, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("NCO Professional Development Ribbon", {awardPriority: 33, awardAttachmentType: "ncoNums", awardType: "Ribbon"}); - this.awards.set("Honor Graduate Ribbon", {awardPriority: 34, awardType: "Ribbon"}); - this.awards.set("Army Service Ribbon", {awardPriority: 35, awardType: "Ribbon"}); - this.awards.set("Cavalry Centurion Medal", {awardPriority: 36, medalPriority: 32, awardAttachmentType: "silverStars", awardType: "Medal"}); - this.awards.set("United Nations Service Medal", {awardPriority: 37, medalPriority: 33, awardAttachmentType: "stars", awardType: "Medal"}); - this.awards.set("Overseas Service Ribbon", {awardPriority: 38, medalPriority: 34, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Ready or Not Service Ribbon",{awardPriority: 39, medalPriority: 35, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("DCS World Service Ribbon", {awardPriority: 40, medalPriority: 36, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Squad Service Ribbon", {awardPriority: 41, medalPriority: 37, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("WWII Service Ribbon", {awardPriority: 42, medalPriority: 38, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Hell Let Loose Service Ribbon", {awardPriority: 43, medalPriority: 39, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Hell Let Loose Console Service Ribbon", {awardPriority: 44, medalPriority: 40, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Battlefield 6 Service Ribbon", {awardPriority: 45, medalPriority: 41, awardAttachmentType: "oakClustersService", awardType: "Medal"}); - this.awards.set("Recruiting Ribbon", {awardPriority: 46, awardAttachmentType: "starsDonation", awardType: "RibbonDonationLogic"}); // May Also require Special Case - this.awards.set("D-Day Commemorative Medal", {awardPriority: 47, medalPriority: 42, awardType: "Medal"}); - this.awards.set("Ranger Selection Ribbon", {awardPriority: 48, awardType: "Ribbon"}); - this.awards.set("Sniper Ribbon", {awardPriority: 49, medalPriority: 43, awardType: "Medal"}); - this.awards.set("Basic Assault Course Ribbon", {awardPriority: 50, awardType: "Ribbon"}); - - // ___ DISCONTINUED RIBBONS/MEDALS WITHOUT PRECIDENCE ___ - // These ones are a bit of an unknown precidence wise. Indeed we have some discon awards above, however precidence is known - // Anything here is shown as is, and there is no inherent precicence for these. - - this.awards.set("Cadre Course Ribbon", {awardPriority: 51, awardType: "Ribbon"}); - this.awards.set("Womens Army Corp Service Medal", {awardPriority: 52, medalPriority:44, awardType: "Medal"}) - this.awards.set("D Day Participation Ribbon", {awardPriority: 53, awardType: "Ribbon"}) - this.awards.set("European/African/Middle Eastern Campaign Medal", {awardPriority: 54, medalPriority:45, awardAttachmentType: "oakClusters", awardType: "Medal"}) - - - //____ UNIT CITATIONS ____ - - this.awards.set("Army & Air Force Presidential Unit Citation", {awardPriority: 0, awardAttachmentType: "unitCitationClusters", awardType: "UnitCitation"}); - this.awards.set("Army Valorous Unit Citation", {awardPriority: 1, awardAttachmentType: "unitCitationClusters", awardType: "UnitCitation"}); - this.awards.set("Joint Meritorious Unit Citation", {awardPriority: 2, awardAttachmentType: "unitCitationClusters", awardType: "UnitCitation"}); - this.awards.set("Army Meritorious Unit Citation", {awardPriority: 3, awardAttachmentType: "unitCitationClusters", awardType: "UnitCitation"}); - this.awards.set("Army Superior Unit Citation", {awardPriority: 4, awardAttachmentType: "unitCitationClusters", awardType: "UnitCitation"}); - this.awards.set("7th Cavalry Black Ops Unit Citation", {awardPriority: 5, awardAttachmentType: "unitCitationSStars", awardType: "UnitCitation"}); - - // ____ COMBAT BADGES ____ - - this.awards.set("Flight Medic Badge", {awardPriority: 6, awardType: "BadgeCombat"}); // (3/1/b/1-7) (4/1/b/1-7) - this.awards.set("Master Army Aviator Badge", {awardPriority: 11, awardType: "BadgeCombat"}); // (A/1-7) (A/ACD) - this.awards.set("Senior Army Aviator Badge", {awardPriority: 10, awardType: "BadgeCombat"}) - this.awards.set("Army Aviator Badge", {awardPriority: 9, awardType: "BadgeCombat"}) - this.awards.set("Aircraft Master Crewman Badge", {awardPriority: 8, awardType: "BadgeCombat"}); // (A/1-7) (A/ACD) - this.awards.set("Aircraft Senior Crewman Badge", {awardPriority: 7, awardType: "BadgeCombat"}) - this.awards.set("Aircraft Crewman Badge", {awardPriority: 6, awardType: "BadgeCombat"}) - this.awards.set("Combat Infantry Badge 4th Award", {awardPriority: 5, awardType: "BadgeCombat"}) - this.awards.set("Combat Infantry Badge 2nd Award", {awardPriority: 3, awardType: "BadgeCombat"}) - this.awards.set("Combat Infantry Badge 3rd Award", {awardPriority: 4, awardType: "BadgeCombat"}) - this.awards.set("Combat Infantry Badge", {awardPriority: 2, awardType: "BadgeCombat"}) - this.awards.set("Expert Infantry Badge", {awardPriority: 1, awardType: "BadgeCombat"}) // et. al. - - //____ WEAPON QUALS ____ - - this.awards.set("Rifle Expert", {awardTag: "rifle", awardType: "WeaponQual"}); - this.awards.set("Rifle Sharpshooter", {awardTag: "rifle", awardType: "WeaponQual"}); - this.awards.set("Rifle Marksman", {awardTag: "rifle", awardType: "WeaponQual"}); - this.awards.set("Grenade Expert", {awardTag: "grenade", awardType: "WeaponQual"}); - this.awards.set("Grenade Sharpshooter", {awardTag: "grenade", awardType: "WeaponQual"}); - this.awards.set("Grenade Marksman", {awardTag: "grenade", awardType: "WeaponQual"}); - this.awards.set("Pistol Expert", {awardTag: "pistol", awardType: "WeaponQual"}); - this.awards.set("Pistol Sharpshooter", {awardTag: "pistol", awardType: "WeaponQual"}); - this.awards.set("Pistol Marksman", {awardTag: "pistol", awardType: "WeaponQual"}); - this.awards.set("M-203 Expert", {awardTag: "m203", awardType: "WeaponQual"}); - this.awards.set("M-203 Sharpshooter", {awardTag: "m203", awardType: "WeaponQual"}); - this.awards.set("M-203 Marksman", {awardTag: "m203", awardType: "WeaponQual"}); - this.awards.set("Machine Gun Expert", {awardTag: "machineGun", awardType: "WeaponQual"}); - this.awards.set("Machine Gun Sharpshooter", {awardTag: "machineGun", awardType: "WeaponQual"}); - this.awards.set("Machine Gun Marksman", {awardTag: "machineGun", awardType: "WeaponQual"}); - this.awards.set("Recoilless Rifle Expert", {awardTag: "recoilless", awardType: "WeaponQual"}); - this.awards.set("Recoilless Rifle Sharpshooter", {awardTag: "recoilless", awardType: "WeaponQual"}); - this.awards.set("Recoilless Rifle Marksman", {awardTag: "recoilless", awardType: "WeaponQual"}); - this.awards.set("Aeroweapons Expert", {awardTag: "aeroweapons", awardType: "WeaponQual"}); - this.awards.set("Aeroweapons Sharpshooter", {awardTag: "aeroweapons", awardType: "WeaponQual"}); - this.awards.set("Aeroweapons Marksman", {awardTag: "aeroweapons", awardType: "WeaponQual"}); - this.awards.set("Hydra-70 Expert", {awardTag: "hydra70", awardType: "WeaponQual"}); - this.awards.set("Hydra-70 Sharpshooter", {awardTag: "hydra70", awardType: "WeaponQual"}); - this.awards.set("Hydra-70 Marksman", {awardTag: "hydra70", awardType: "WeaponQual"}); - this.awards.set("Tank Weapons Expert", {awardTag: "tankWeapons", awardType: "WeaponQual"}); - this.awards.set("Tank Weapons Sharpshooter", {awardTag: "tankWeapons", awardType: "WeaponQual"}); - this.awards.set("Tank Weapons Marksman", {awardTag: "tankWeapons", awardType: "WeaponQual"}); - - //____ TABS ____ - - this.awards.set("Special Forces Tab", {awardPriority: 0, awardType: "Tab"}); - this.awards.set("Ranger Tab", {awardPriority: 1, awardType: "Tab"}); - this.awards.set("Sapper Tab", {awardPriority: 2, awardType: "Tab"}); - this.awards.set("Long-Range Reconnaissance Patrol Tab", {awardPriority: 3, awardType: "Tab"}); - + for (const { name, ...details } of AWARD_CATALOG) { + this.awards.set(name, details); + } } isInRegistry(awardName) { @@ -139,41 +22,16 @@ export class AwardRegistry { } getAwardDetails(awardName) { - if (awardName.includes("with Valor Device")) { - awardName = awardName.replace(" with Valor Device", ""); - } - - if (this.awards.get(awardName) == undefined) { - return 0; + if (hasValorDevice(awardName)) { + awardName = stripValorDevice(awardName); } - return this.awards.get(awardName); + return this.awards.get(awardName) ?? 0; } getMaxAwardCount(awardName) { - switch (this.getAwardDetails(awardName).awardAttachmentType) { - case "oakClusters": - return 19; - case "unitCitationClusters": - return 10; - case "unitCitationSStars": - return 5; - case "oakClustersService": - return 6; - case "oakClustersValor": - return 14; - case "silverStars": - return 5; - case "stars": - return 10; - case "starsDonation": //Requires advanced logic - return 12; - case "gcNotches": - return 9; - case "ncoNums": - return 6; - default: - return 1; - } + return ( + MAX_AWARD_COUNT[this.getAwardDetails(awardName).awardAttachmentType] ?? 1 + ); } } diff --git a/client/app/uniformbuilder/modules/GetUserInfo.jsx b/client/app/uniformbuilder/modules/GetUserInfo.jsx index f0c1500..05e8a8b 100644 --- a/client/app/uniformbuilder/modules/GetUserInfo.jsx +++ b/client/app/uniformbuilder/modules/GetUserInfo.jsx @@ -3,6 +3,7 @@ import GetCitationCoordArray from "./getCitationCoordArray"; import GetCombatBadgeCoords from "./getCombatBadgeCoords"; import GetYearsInServiceCoordArray from "./getYearsInServiceCoordArray"; import GetTabCoordArray from "./getTabCoordArray"; +import { Mos } from "./constants"; export default function GetUserInfo( dataActive, @@ -110,28 +111,28 @@ function getRankGrade(rankId) { function setShoulderCord(mos) { switch (mos) { - case "68W": - case "67A": + case Mos.M68W: + case Mos.M67A: return "Medical"; - case "11B": - case "11A": - case "11C": + case Mos.M11B: + case Mos.M11A: + case Mos.M11C: return "Infantry"; - case "13A": - case "13B": + case Mos.M13A: + case Mos.M13B: return "Artillery"; - case "12A": - case "12B": + case Mos.M12A: + case Mos.M12B: return "Engineer"; - case "01A": + case Mos.M01A: return "Aide"; - case "31A": - case "31B": + case Mos.M31A: + case Mos.M31B: return "MP"; - case "19K": - case "19A": - case "19C": - case "19D": + case Mos.M19K: + case Mos.M19A: + case Mos.M19C: + case Mos.M19D: return "Armor"; default: return false; @@ -140,79 +141,79 @@ function setShoulderCord(mos) { function setNeckPins(mos) { switch (mos) { - case "153A": - case "155A": - case "15A": + case Mos.M153A: + case Mos.M155A: + case Mos.M15A: return "AviationOfficer"; - case "15T": - case "155F": + case Mos.M15T: + case Mos.M155F: return "AviationNCO"; - case "13A": + case Mos.M13A: return "ArtilleryOfficer"; - case "13B": + case Mos.M13B: return "ArtilleryNCO"; - case "67A": + case Mos.M67A: return "MedicalOfficer"; - case "68W": + case Mos.M68W: return "MedicalNCO"; - case "12A": + case Mos.M12A: return "EngineerOfficer"; - case "12B": + case Mos.M12B: return "EngineerNCO"; - case "01A": + case Mos.M01A: return "Aide"; - case "00Z": + case Mos.M00Z: return "CSM"; - case "255N": - case "25A": + case Mos.M255N: + case Mos.M25A: return "IMOOfficer"; - case "25U": + case Mos.M25U: return "IMONCO"; - case "42B": - case "57A": - case "46A": + case Mos.M42B: + case Mos.M57A: + case Mos.M46A: return "S1S3S5"; - case "35B": + case Mos.M35B: return "S2NCO"; - case "35A": + case Mos.M35A: return "S2Officer"; - case "31A": + case Mos.M31A: return "MPOfficer"; - case "31B": + case Mos.M31B: return "MPNCO"; - case "19A": + case Mos.M19A: return "ArmorOfficer"; - case "19K": - case "19C": - case "19D": + case Mos.M19K: + case Mos.M19C: + case Mos.M19D: return "ArmorNCO"; - case "27A": + case Mos.M27A: return "JAGOfficer"; - case "27D": + case Mos.M27D: return "JAGNCO"; - case "51A": - case "50A": + case Mos.M51A: + case Mos.M50A: return "RDCOfficer"; - case "11A": - case "13A": - case "47A": - case "79A": - case "79Z": - case "26Z": - case "47Q": - case "47C": + case Mos.M11A: + case Mos.M13A: + case Mos.M47A: + case Mos.M79A: + case Mos.M79Z: + case Mos.M26Z: + case Mos.M47Q: + case Mos.M47C: return "InfantryOfficer"; - case "11B": - case "11C": - case "42A": - case "57B": - case "46S": - case "79R": - case "79X": - case "51S": - case "49A": - case "26B": - case "47T": + case Mos.M11B: + case Mos.M11C: + case Mos.M42A: + case Mos.M57B: + case Mos.M46S: + case Mos.M79R: + case Mos.M79X: + case Mos.M51S: + case Mos.M49A: + case Mos.M26B: + case Mos.M47T: return "InfantryNCO"; default: return false; diff --git a/client/app/uniformbuilder/modules/constants/mos.js b/client/app/uniformbuilder/modules/constants/mos.js index 00ce45e..37f6c7e 100644 --- a/client/app/uniformbuilder/modules/constants/mos.js +++ b/client/app/uniformbuilder/modules/constants/mos.js @@ -57,7 +57,7 @@ export const Mos = Object.freeze({ // Semantic groupings used by BadgeCombat badge logic in AwardClasses.jsx. // Membership is the exact set the original if-chains tested — do not add codes. export const MosGroup = Object.freeze({ - AVIATION: [Mos.M153A, Mos.M155A, Mos.M15A, Mos.M15T], + AVIATION: [Mos.M153A, Mos.M155A, Mos.M15A, Mos.M15T, Mos.M155F], MEDICAL: [Mos.M68W, Mos.M67A], - AIRCREW: [Mos.M15T, Mos.M155F], + AIRCREW: [Mos.M15T], }); diff --git a/client/app/uniformbuilder/modules/getCanvasObject.jsx b/client/app/uniformbuilder/modules/getCanvasObject.jsx index d9eb83d..9f0bbbf 100644 --- a/client/app/uniformbuilder/modules/getCanvasObject.jsx +++ b/client/app/uniformbuilder/modules/getCanvasObject.jsx @@ -13,6 +13,12 @@ import { Tab, } from "./AwardClasses"; import GetUserInfo from "./GetUserInfo"; +import { + AwardType, + AwardAttachmentType, + hasValorDevice, + stripValorDevice, +} from "./constants"; export default async function GetCanvasObject(userName) { const data = await GetIndividual(userName); @@ -30,28 +36,21 @@ export default async function GetCanvasObject(userName) { //Check to see if the API medal is one with valor. If so, flag it w/ hasValorDevice. //Set the key of the Award to be the Award Name. - let key; - let hasValorDevice = false; + const awardName = data.awards[i].awardName; + const valorDevice = hasValorDevice(awardName); + const key = valorDevice ? stripValorDevice(awardName) : awardName; + let useCombatBadgeLogic = false; let combatBadgeKey; - if (data.awards[i].awardName.includes("with Valor Device")) { - key = data.awards[i].awardName.replace(" with Valor Device", ""); - hasValorDevice = true; - } else { - key = data.awards[i].awardName; - } - const awardType = AwardRegistryInstance.getAwardDetails(key).awardType; - if (awardType == "BadgeCombat") { - useCombatBadgeLogic = true; - combatBadgeKey = "BadgeCombat"; - } - - if (awardType == "WeaponQual") { + if ( + awardType == AwardType.BadgeCombat || + awardType == AwardType.WeaponQual + ) { useCombatBadgeLogic = true; - combatBadgeKey = "WeaponQual"; + combatBadgeKey = awardType; } //If there is already an award with the key, add the valor device to the existing obj if true and increment AttachmentCount @@ -63,11 +62,11 @@ export default async function GetCanvasObject(userName) { if ( awardMap.has(key) || - (useCombatBadgeLogic == true && awardMap.has(combatBadgeKey)) + (useCombatBadgeLogic && awardMap.has(combatBadgeKey)) ) { let existingAward; - if (useCombatBadgeLogic == true) { + if (useCombatBadgeLogic) { existingAward = awardMap.get(combatBadgeKey); } else { existingAward = awardMap.get(key); @@ -78,9 +77,10 @@ export default async function GetCanvasObject(userName) { } if (existingAward instanceof MedalWithValor) { - if (hasValorDevice == true) { + if (valorDevice) { existingAward.hasValorDevice = true; - existingAward.ribbonAttachmentType = "oakClustersValor"; + existingAward.ribbonAttachmentType = + AwardAttachmentType.OAK_CLUSTERS_VALOR; } } @@ -111,12 +111,12 @@ export default async function GetCanvasObject(userName) { //This can probably be written better, but thats a later problem if (AwardRegistryInstance.isInRegistry(key)) { switch (awardDetails.awardType) { - case "Ribbon": + case AwardType.Ribbon: const newRibbon = new Ribbon(data.awards[i], AwardRegistryInstance); awardMap.set(key, newRibbon); totalRibbonCount++; break; - case "RibbonDonationLogic": + case AwardType.RibbonDonationLogic: const newRibbonDonation = new RibbonDonationLogic( data.awards[i], AwardRegistryInstance, @@ -124,12 +124,12 @@ export default async function GetCanvasObject(userName) { awardMap.set(key, newRibbonDonation); totalRibbonCount++; break; - case "Medal": + case AwardType.Medal: const newMedal = new Medal(data.awards[i], AwardRegistryInstance); awardMap.set(key, newMedal); totalRibbonCount++; break; - case "MedalTiered": + case AwardType.MedalTiered: const newTiered = new MedalTiered( data.awards[i], AwardRegistryInstance, @@ -137,7 +137,7 @@ export default async function GetCanvasObject(userName) { awardMap.set(key, newTiered); totalRibbonCount++; break; - case "MedalWithValor": + case AwardType.MedalWithValor: const newMedalWithValor = new MedalWithValor( data.awards[i], AwardRegistryInstance, @@ -145,7 +145,7 @@ export default async function GetCanvasObject(userName) { awardMap.set(key, newMedalWithValor); totalRibbonCount++; break; - case "UnitCitation": + case AwardType.UnitCitation: const newUnitCitation = new UnitCitation( data.awards[i], AwardRegistryInstance, @@ -153,30 +153,27 @@ export default async function GetCanvasObject(userName) { awardMap.set(key, newUnitCitation); totalUnitCitationCount++; break; - case "BadgeCombat": + case AwardType.BadgeCombat: const newBadgeCombat = new BadgeCombat( data.awards[i], data.mos, AwardRegistryInstance, ); - awardMap.set("BadgeCombat", newBadgeCombat); + awardMap.set(AwardType.BadgeCombat, newBadgeCombat); break; - case "WeaponQual": + case AwardType.WeaponQual: const newWeaponQual = new WeaponQual( data.awards[i], AwardRegistryInstance, ); - awardMap.set("WeaponQual", newWeaponQual); + awardMap.set(AwardType.WeaponQual, newWeaponQual); break; - case "Tab": + case AwardType.Tab: const newTab = new Tab(data.awards[i], AwardRegistryInstance); tabCount++; awardMap.set(key, newTab); break; } - } else { - // const newAward = new Award(data.awards[i]); - // awardMap.set(key, newAward); } } } @@ -221,9 +218,7 @@ export default async function GetCanvasObject(userName) { } } - if (weaponQual == null) { - weaponQual = 0; - } + weaponQual = weaponQual ?? 0; arr.push(ribbons.sort((a, b) => a.awardPriority - b.awardPriority)); arr.push(unitCitations.sort((a, b) => a.awardPriority - b.awardPriority)); @@ -232,8 +227,5 @@ export default async function GetCanvasObject(userName) { arr.push(weaponQual); arr.push(tabs.sort((a, b) => a.awardPriority - b.awardPriority)); - console.log(arr); - console.log(data.mos); - return arr; } From 2b966c02ab0f2969b50edf1c7a23da0c9b4144ac Mon Sep 17 00:00:00 2001 From: Jens Gryspeert Date: Tue, 2 Jun 2026 14:31:51 +0200 Subject: [PATCH 3/3] refactor(uniformbuilder): key MOS enum by official 7cav titles Rename the Mos enum keys from M to the official S1 MOS titles from the 7cav wiki (e.g. ARMOR_CREWMAN: "19K", COMBAT_MEDIC: "68W"). Values stay the exact code strings, so all switch cases and membership checks are unchanged. S2 keeps three entries: S2_OFFICER (35A), S2_NCO (35B) and S2_ENLISTED (35F). 35F is added from the wiki list (no consumer yet, so inert); 35B is retained because the roster still uses it. Behavior-identical: every MOS-to-role mapping and the aviation badge logic resolve to the same codes as before. --- .../uniformbuilder/modules/AwardClasses.jsx | 3 +- .../uniformbuilder/modules/GetUserInfo.jsx | 132 ++++++++-------- .../uniformbuilder/modules/constants/mos.js | 145 +++++++++++------- 3 files changed, 155 insertions(+), 125 deletions(-) diff --git a/client/app/uniformbuilder/modules/AwardClasses.jsx b/client/app/uniformbuilder/modules/AwardClasses.jsx index c082107..a64caff 100644 --- a/client/app/uniformbuilder/modules/AwardClasses.jsx +++ b/client/app/uniformbuilder/modules/AwardClasses.jsx @@ -1,6 +1,5 @@ import { AwardAttachmentType, - Mos, MosGroup, AwardNameFragment, hasValorDevice, @@ -227,7 +226,7 @@ export class BadgeCombat extends Badge { //we need to give 15T an exception so that they stop at aircrew badges. if (this.isAviation) { - if (this.userMos == Mos.M15T) { + if (MosGroup.AIRCREW.includes(this.userMos)) { this.maxAllowed = 8; } else { this.maxAllowed = 11; diff --git a/client/app/uniformbuilder/modules/GetUserInfo.jsx b/client/app/uniformbuilder/modules/GetUserInfo.jsx index 05e8a8b..966517c 100644 --- a/client/app/uniformbuilder/modules/GetUserInfo.jsx +++ b/client/app/uniformbuilder/modules/GetUserInfo.jsx @@ -111,28 +111,28 @@ function getRankGrade(rankId) { function setShoulderCord(mos) { switch (mos) { - case Mos.M68W: - case Mos.M67A: + case Mos.COMBAT_MEDIC: + case Mos.MEDICAL_OFFICER: return "Medical"; - case Mos.M11B: - case Mos.M11A: - case Mos.M11C: + case Mos.INFANTRYMAN: + case Mos.INFANTRY_OFFICER: + case Mos.INDIRECT_FIRE_INFANTRYMAN: return "Infantry"; - case Mos.M13A: - case Mos.M13B: + case Mos.FIELD_ARTILLERY_OFFICER: + case Mos.CANNON_CREWMEMBER: return "Artillery"; - case Mos.M12A: - case Mos.M12B: + case Mos.COMBAT_ENGINEER_OFFICER: + case Mos.COMBAT_ENGINEER: return "Engineer"; - case Mos.M01A: + case Mos.OFFICER_GENERALIST: return "Aide"; - case Mos.M31A: - case Mos.M31B: + case Mos.MP_OFFICER: + case Mos.MP_ENLISTED: return "MP"; - case Mos.M19K: - case Mos.M19A: - case Mos.M19C: - case Mos.M19D: + case Mos.ARMOR_CREWMAN: + case Mos.ARMOR_CAVALRY_OFFICER: + case Mos.BRADLEY_CREWMEMBER: + case Mos.CAVALRY_SCOUT: return "Armor"; default: return false; @@ -141,79 +141,79 @@ function setShoulderCord(mos) { function setNeckPins(mos) { switch (mos) { - case Mos.M153A: - case Mos.M155A: - case Mos.M15A: + case Mos.ROTARY_WING_AVIATOR_WARRANT_OFFICER: + case Mos.FIXED_WING_AVIATOR_WARRANT_OFFICER: + case Mos.AVIATION_OFFICER: return "AviationOfficer"; - case Mos.M15T: - case Mos.M155F: + case Mos.ENLISTED_ROTARY_CREWMAN: + case Mos.JET_AIRCRAFT_PILOT: return "AviationNCO"; - case Mos.M13A: + case Mos.FIELD_ARTILLERY_OFFICER: return "ArtilleryOfficer"; - case Mos.M13B: + case Mos.CANNON_CREWMEMBER: return "ArtilleryNCO"; - case Mos.M67A: + case Mos.MEDICAL_OFFICER: return "MedicalOfficer"; - case Mos.M68W: + case Mos.COMBAT_MEDIC: return "MedicalNCO"; - case Mos.M12A: + case Mos.COMBAT_ENGINEER_OFFICER: return "EngineerOfficer"; - case Mos.M12B: + case Mos.COMBAT_ENGINEER: return "EngineerNCO"; - case Mos.M01A: + case Mos.OFFICER_GENERALIST: return "Aide"; - case Mos.M00Z: + case Mos.COMMAND_SERGEANT_MAJOR: return "CSM"; - case Mos.M255N: - case Mos.M25A: + case Mos.REGIMENTAL_TECHNICAL_AIDE: + case Mos.S6_OFFICER: return "IMOOfficer"; - case Mos.M25U: + case Mos.S6_ENLISTED: return "IMONCO"; - case Mos.M42B: - case Mos.M57A: - case Mos.M46A: + case Mos.S1_OFFICER: + case Mos.S3_OFFICER: + case Mos.S5_OFFICER: return "S1S3S5"; - case Mos.M35B: + case Mos.S2_NCO: return "S2NCO"; - case Mos.M35A: + case Mos.S2_OFFICER: return "S2Officer"; - case Mos.M31A: + case Mos.MP_OFFICER: return "MPOfficer"; - case Mos.M31B: + case Mos.MP_ENLISTED: return "MPNCO"; - case Mos.M19A: + case Mos.ARMOR_CAVALRY_OFFICER: return "ArmorOfficer"; - case Mos.M19K: - case Mos.M19C: - case Mos.M19D: + case Mos.ARMOR_CREWMAN: + case Mos.BRADLEY_CREWMEMBER: + case Mos.CAVALRY_SCOUT: return "ArmorNCO"; - case Mos.M27A: + case Mos.JAG_OFFICER: return "JAGOfficer"; - case Mos.M27D: + case Mos.JAG_ENLISTED: return "JAGNCO"; - case Mos.M51A: - case Mos.M50A: + case Mos.DEVCOM_LEAD: + case Mos.RDC_OFFICER: return "RDCOfficer"; - case Mos.M11A: - case Mos.M13A: - case Mos.M47A: - case Mos.M79A: - case Mos.M79Z: - case Mos.M26Z: - case Mos.M47Q: - case Mos.M47C: + case Mos.INFANTRY_OFFICER: + case Mos.FIELD_ARTILLERY_OFFICER: + case Mos.S7_OFFICER: + case Mos.RRD_OFFICER: + case Mos.RTC_OFFICER: + case Mos.WAG_OFFICER: + case Mos.ODS_OFFICER: + case Mos.NCOA_OFFICER: return "InfantryOfficer"; - case Mos.M11B: - case Mos.M11C: - case Mos.M42A: - case Mos.M57B: - case Mos.M46S: - case Mos.M79R: - case Mos.M79X: - case Mos.M51S: - case Mos.M49A: - case Mos.M26B: - case Mos.M47T: + case Mos.INFANTRYMAN: + case Mos.INDIRECT_FIRE_INFANTRYMAN: + case Mos.S1_ENLISTED: + case Mos.S3_ENLISTED: + case Mos.S5_ENLISTED: + case Mos.RRD_ENLISTED: + case Mos.RTC_ENLISTED: + case Mos.DEVCOM_SUPPORT_COORDINATOR: + case Mos.FCC_ANALYST: + case Mos.WAG_ENLISTED: + case Mos.NCOA_ENLISTED: return "InfantryNCO"; default: return false; diff --git a/client/app/uniformbuilder/modules/constants/mos.js b/client/app/uniformbuilder/modules/constants/mos.js index 37f6c7e..7f1c80d 100644 --- a/client/app/uniformbuilder/modules/constants/mos.js +++ b/client/app/uniformbuilder/modules/constants/mos.js @@ -1,63 +1,94 @@ -// Military Occupational Specialty codes. Keys are the code prefixed with `M` -// (codes start with a digit and can't be bare identifiers); values are the -// exact API strings. Covers every code referenced in AwardClasses.jsx and -// GetUserInfo.jsx. +// Military Occupational Specialty codes. Keys are the official 7cav S1 MOS +// name (UPPER_SNAKE); values are the exact code string used by the API and the +// rest of the app. Names sourced from the 7th Cavalry wiki "Approved MOS List": +// https://wiki.7cav.us/wiki/Military_Occupational_Specialty_(MOS) +// +// S2 has three entries by design: S2_OFFICER (35A), S2_NCO (35B) and +// S2_ENLISTED (35F). 35B is not on the wiki list (which only shows 35F for S2 +// Enlisted) but is kept because the roster still uses it. export const Mos = Object.freeze({ - M00Z: "00Z", - M01A: "01A", - M11A: "11A", - M11B: "11B", - M11C: "11C", - M12A: "12A", - M12B: "12B", - M13A: "13A", - M13B: "13B", - M15A: "15A", - M15T: "15T", - M153A: "153A", - M155A: "155A", - M155F: "155F", - M19A: "19A", - M19C: "19C", - M19D: "19D", - M19K: "19K", - M25A: "25A", - M25U: "25U", - M255N: "255N", - M26B: "26B", - M26Z: "26Z", - M27A: "27A", - M27D: "27D", - M31A: "31A", - M31B: "31B", - M35A: "35A", - M35B: "35B", - M42A: "42A", - M42B: "42B", - M46A: "46A", - M46S: "46S", - M47A: "47A", - M47C: "47C", - M47Q: "47Q", - M47T: "47T", - M49A: "49A", - M50A: "50A", - M51A: "51A", - M51S: "51S", - M57A: "57A", - M57B: "57B", - M67A: "67A", - M68W: "68W", - M79A: "79A", - M79R: "79R", - M79X: "79X", - M79Z: "79Z", + // Aviation + AVIATION_OFFICER: "15A", + ROTARY_WING_AVIATOR_WARRANT_OFFICER: "153A", + FIXED_WING_AVIATOR_WARRANT_OFFICER: "155A", + JET_AIRCRAFT_PILOT: "155F", + ENLISTED_ROTARY_CREWMAN: "15T", + // Infantry + INFANTRY_OFFICER: "11A", + INFANTRYMAN: "11B", + INDIRECT_FIRE_INFANTRYMAN: "11C", + // Medical + MEDICAL_OFFICER: "67A", + COMBAT_MEDIC: "68W", + // Armor / Cavalry + ARMOR_CAVALRY_OFFICER: "19A", + BRADLEY_CREWMEMBER: "19C", + CAVALRY_SCOUT: "19D", + ARMOR_CREWMAN: "19K", + // Engineer + COMBAT_ENGINEER_OFFICER: "12A", + COMBAT_ENGINEER: "12B", + // Field Artillery + FIELD_ARTILLERY_OFFICER: "13A", + CANNON_CREWMEMBER: "13B", + // Command + COMMAND_SERGEANT_MAJOR: "00Z", + OFFICER_GENERALIST: "01A", + // Staff — S1 + S1_OFFICER: "42B", + S1_ENLISTED: "42A", + // Staff — S2 + S2_OFFICER: "35A", + S2_NCO: "35B", + S2_ENLISTED: "35F", + // Staff — S3 + S3_OFFICER: "57A", + S3_ENLISTED: "57B", + // Staff — S5 + S5_OFFICER: "46A", + S5_ENLISTED: "46S", + // Staff — S6 + S6_OFFICER: "25A", + S6_ENLISTED: "25U", + REGIMENTAL_TECHNICAL_AIDE: "255N", + // Staff — S7 + S7_OFFICER: "47A", + // Military Police + MP_OFFICER: "31A", + MP_ENLISTED: "31B", + // JAG + JAG_OFFICER: "27A", + JAG_ENLISTED: "27D", + // RRD + RRD_OFFICER: "79A", + RRD_ENLISTED: "79R", + // RTC + RTC_OFFICER: "79Z", + RTC_ENLISTED: "79X", + // WAG + WAG_OFFICER: "26Z", + WAG_ENLISTED: "26B", + // Other commands / schools + ODS_OFFICER: "47Q", + NCOA_OFFICER: "47C", + NCOA_ENLISTED: "47T", + FCC_ANALYST: "49A", + RDC_OFFICER: "50A", + DEVCOM_LEAD: "51A", + DEVCOM_SUPPORT_COORDINATOR: "51S", }); // Semantic groupings used by BadgeCombat badge logic in AwardClasses.jsx. -// Membership is the exact set the original if-chains tested — do not add codes. +// Membership matches the post-#129 aviation behavior (155F counts as aviation, +// only 15T is aircrew). Do not change membership without checking that logic. export const MosGroup = Object.freeze({ - AVIATION: [Mos.M153A, Mos.M155A, Mos.M15A, Mos.M15T, Mos.M155F], - MEDICAL: [Mos.M68W, Mos.M67A], - AIRCREW: [Mos.M15T], + AVIATION: [ + Mos.ROTARY_WING_AVIATOR_WARRANT_OFFICER, + Mos.FIXED_WING_AVIATOR_WARRANT_OFFICER, + Mos.AVIATION_OFFICER, + Mos.ENLISTED_ROTARY_CREWMAN, + Mos.JET_AIRCRAFT_PILOT, + ], + MEDICAL: [Mos.COMBAT_MEDIC, Mos.MEDICAL_OFFICER], + AIRCREW: [Mos.ENLISTED_ROTARY_CREWMAN], });