(function () {
"use strict";
var PROPOSAL_BOT_IDML_BASE_FILES = {
"designmap.xml": "\n\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\tMinion Pro\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\tBlack\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\tLightBlue\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\tArabic\n\t\t\n\t\n\t\n\t\t\n\t\t\tGold\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t$ID/\n\t\t\t$ID/\n\t\t\n\t\n\t\n\t\t\n\t\t\t$ID/\n\t\t\t$ID/\n\t\t\n\t\n\t\n\t\t\n\t\t\t$ID/\n\t\t\t$ID/\n\t\t\n\t\n\t\n\t\t\n\t\t\tMyriad Pro\n\t\t\t$ID/Regular\n\t\t\n\t\n\t\n\t\t\n\t\t\tMinion Pro\n\t\t\t$ID/Regular\n\t\t\n\t\n\t\n\t\t\n\t\t\tNothing\n\t\t\n\t\n\n",
"META-INF/container.xml": "\n\n\t\n\t\t\n\t\t\n\t\n\n",
"META-INF/metadata.xml": "\n\n\n \n \n application/x-indesign\n \n \n 2012-03-14T14:14:53+01:00\n 2012-03-14T16:05:17+01:00\n 2012-03-14T16:05:17+01:00\n Adobe InDesign 7.5\n \n \n xmp.iid:FA7F11740720681188C6DF1D8A35579E\n xmp.did:F77F11740720681188C6DF1D8A35579E\n xmp.did:F77F11740720681188C6DF1D8A35579E\n \n \n \n created\n xmp.iid:F77F11740720681188C6DF1D8A35579E\n 2012-03-14T14:14:53+01:00\n Adobe InDesign 7.5\n \n \n saved\n xmp.iid:F87F11740720681188C6DF1D8A35579E\n 2012-03-14T14:17:50+01:00\n Adobe InDesign 7.5\n /;/metadata\n \n \n saved\n xmp.iid:F97F11740720681188C6DF1D8A35579E\n 2012-03-14T14:17:50+01:00\n Adobe InDesign 7.5\n /metadata\n \n \n saved\n xmp.iid:FA7F11740720681188C6DF1D8A35579E\n 2012-03-14T16:05:17+01:00\n Adobe InDesign 7.5\n /;/metadata\n \n \n \n default\n \n \n 445\n \n \n \n \n \n Black\n CMYK\n Process\n 0\n 0\n 0\n 100\n \n \n Paper\n CMYK\n Process\n 0\n 0\n 0\n 0\n \n \n Registration\n CMYK\n Process\n 100\n 100\n 100\n 100\n \n \n \n \n \n \n MinionPro-BoldCn\n Minion Pro\n Bold Cond\n OpenTypeCFF\n MinionPro-BoldCnVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-BoldCn.otf\n \n \n MinionPro-BoldCnIt\n Minion Pro\n Bold Cond Italic\n OpenTypeCFF\n MinionPro-BoldCnItVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-BoldCnIt.otf\n \n \n MinionPro-Regular\n Minion Pro\n Regular\n OpenTypeCFF\n MinionPro-RegularVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-Regular.otf\n \n \n MinionPro-It\n Minion Pro\n Italic\n OpenTypeCFF\n MinionPro-ItVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-It.otf\n \n \n MinionPro-Medium\n Minion Pro\n Medium\n OpenTypeCFF\n MinionPro-MediumVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-Medium.otf\n \n \n MinionPro-MediumIt\n Minion Pro\n Medium Italic\n OpenTypeCFF\n MinionPro-MediumItVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-MediumIt.otf\n \n \n MinionPro-Semibold\n Minion Pro\n Semibold\n OpenTypeCFF\n MinionPro-SemiboldVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-Semibold.otf\n \n \n MinionPro-SemiboldIt\n Minion Pro\n Semibold Italic\n OpenTypeCFF\n MinionPro-SemiboldItVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-SemiboldIt.otf\n \n \n MinionPro-Bold\n Minion Pro\n Bold\n OpenTypeCFF\n MinionPro-BoldVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-Bold.otf\n \n \n MinionPro-BoldIt\n Minion Pro\n Bold Italic\n OpenTypeCFF\n MinionPro-BoldItVersion 2.068;PS 2.000;hotconv 1.0.57;makeotf.lib2.0.21895\n false\n MinionPro-BoldIt.otf\n \n \n Vollkorn-Italic\n Vollkorn\n Italic\n TrueType\n Vollkorn-ItalicVersion 2.001\n false\n Vollkorn-Italic.ttf\n \n \n Vollkorn-Regular\n Vollkorn\n Regular\n TrueType\n Vollkorn-RegularVersion 2.001\n false\n Vollkorn-Regular.ttf\n \n \n Vollkorn-Bold\n Vollkorn\n Bold\n TrueType\n Vollkorn-BoldVersion 2.001\n false\n Vollkorn-Bold.ttf\n \n \n Vollkorn-BoldItalic\n Vollkorn\n Bold Italic\n TrueType\n Vollkorn-BoldItalicVersion 2.001\n false\n Vollkorn-BoldItalic.ttf\n \n \n \n \n \n\n",
"Resources/Graphic.xml": "\n\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\n",
"Resources/Fonts.xml": "\n\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\tKozuka Mincho Pro\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tKozuka Mincho Pro\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tKozuka Mincho Pro\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tKozuka Mincho Pro\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tMinion Pro\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\tMinion Pro\n\t\t\t\n\t\t\n\t\n",
"Resources/Styles.xml": "\n\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\tAuto\n\t\t\t\tMinion Pro\n\t\t\t\tText Color\n\t\t\t\tText Color\n\t\t\t\tStrokeStyle/$ID/Solid\n\t\t\t\tStrokeStyle/$ID/Solid\n\t\t\t\tNoBalancing\n\t\t\t\tSwatch/None\n\t\t\t\tSwatch/None\n\t\t\t\tText Color\n\t\t\t\tSwatch/None\n\t\t\t\tStrokeStyle/$ID/Solid\n\t\t\t\tText Color\n\t\t\t\tSwatch/None\n\t\t\t\tStrokeStyle/$ID/Solid\n\t\t\t\tAll\n\t\t\t\tNothing\n\t\t\t\tNothing\n\t\t\t\t$ID/\n\t\t\t\tNothing\n\t\t\t\tText Color\n\t\t\t\tText Color\n\t\t\t\t$ID/\n\t\t\t\tNothing\n\t\t\t\tText Color\n\t\t\t\tText Color\n\t\t\t\t\n\t\t\t\t1, 2, 3, 4...\n\t\t\t\t$ID/\n\t\t\t\tNothing\n\t\t\t\tNumberingList/$ID/[Default]\n\t\t\t\t\n\t\t\t\tCharacterStyle/$ID/[No character style]\n\t\t\t\tCharacterStyle/$ID/[No character style]\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t$ID/[No paragraph style]\n\t\t\t\tNothing\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\t\n\t\n\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t$ID/[No table style]\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tLightBlue\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t$ID/[None]\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tLightBlue\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t$ID/[None]\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tLightBlue\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\t$ID/[None]\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\t0\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\tLightBlue\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\n",
"Resources/Preferences.xml": "\n\n\t\n\t\t\n\t\t\tBrickRed\n\t\t\tDarkBlue\n\t\t\tGrassGreen\n\t\t\tViolet\n\t\t\n\t\n\t\n\t\t\n\t\t\tStylesheetInXML\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\tNothing\n\t\t\tStylesheetInXML\n\t\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t0\n\t\t\t\t0\n\t\t\t\t0\n\t\t\t\t0\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\tMinion Pro\n\t\t\tAuto\n\t\t\tText Color\n\t\t\tSwatch/None\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tText Color\n\t\t\tSwatch/None\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tNoBalancing\n\t\t\tText Color\n\t\t\tSwatch/None\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tText Color\n\t\t\tSwatch/None\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tAll\n\t\t\tNothing\n\t\t\tNothing\n\t\t\tText Color\n\t\t\tText Color\n\t\t\t$ID/\n\t\t\tNothing\n\t\t\tText Color\n\t\t\tText Color\n\t\t\t$ID/\n\t\t\tNothing\n\t\t\t\n\t\t\t$ID/\n\t\t\tNothing\n\t\t\tCharacterStyle/$ID/[No character style]\n\t\t\tCharacterStyle/$ID/[No character style]\n\t\t\tNumberingList/$ID/[Default]\n\t\t\t1, 2, 3, 4...\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\tLightBlue\n\t\t\n\t\n\t\n\t\t\n\t\t\tArabic\n\t\t\tDontRestart\n\t\t\tNoPrefixSuffix\n\t\t\tSuperscriptMarker\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tColor/Black\n\t\t\tSwatch/None\n\t\t\tStrokeStyle/$ID/Solid\n\t\t\tColor/Black\n\t\t\tSwatch/None\n\t\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\tViolet\n\t\t\tMagenta\n\t\t\n\t\n\t\n\t\t\n\t\t\tLightGray\n\t\t\tLightBlue\n\t\t\n\t\n\t\n\t\t\n\t\t\tCyan\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\tLightGray\n\t\t\tFiesta\n\t\t\tGridBlue\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\tColorQube 9201 "XRX9201_5e"\n\t\t\tXerox ColorQube 9201\n\t\t\tA4\n\t\t\tAuto\n\t\t\tAuto\n\t\t\tDefault\n\t\t\tDefault\n\t\t\tUseDocument\n\t\t\tDefault\n\t\t\tAllPages\n\t\t\t\n\t\t\t\n\t\t\tDefault\n\t\t\n\t\n\t\n\t\t\n\t\t\tAllPages\n\t\t\n\t\n\t\n\t\t\n\t\t\tColorQube 9201 "XRX9201_5e"\n\t\t\tXerox ColorQube 9201\n\t\t\tA4\n\t\t\tAuto\n\t\t\tAuto\n\t\t\tDefault\n\t\t\tDefault\n\t\t\tUseDocument\n\t\t\tDefault\n\t\t\tDefault\n\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\n\t\n\t\n\t\n\t\t\n\t\t\t\n\t\t\n\t\n\t\n\t\t\n\t\t\tMinion Pro\n\t\t\n\t\n\t\n\t\t\n\t\t\tMinion Pro\n\t\t\n\t\n\t\n\t\t\n\t\t\tGridGreen\n\t\t\n\t\n\t\n\t\n\t\t\n\t\t\t1, 2, 3, 4...\n\t\t\n\t\n\t\n\t\n\t\n\t\n",
"XML/Tags.xml": "\n\n\t\n\t\t\n\t\t\tGreen\n\t\t\n\t\n\t\n\t\t\n\t\t\tRed\n\t\t\n\t\n\t\n\t\t\n\t\t\tMagenta\n\t\t\n\t\n\t\n\t\t\n\t\t\tGray\n\t\t\n\t\n\t\n\t\t\n\t\t\tCyan\n\t\t\n\t\n\t\n\t\t\n\t\t\tLightBlue\n\t\t\n\t\n\t\n\t\t\n\t\t\tBrickRed\n\t\t\n\t\n\t\n\t\t\n\t\t\tYellow\n\t\t\n\t\n\t\n\t\t\n\t\t\tBlue\n\t\t\n\t\n",
"XML/BackingStory.xml": "\n\n\t\n\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t\t\ufeff\n\t\t\t\n\t\t\n\t\n"
};
var STORAGE_KEY = "proposal-bot-full-service-drafts-v2";
var LAST_DRAFT_KEY = "proposal-bot-full-service-last-draft-v2";
var THEME_SELECTION_KEY = "proposal-bot-full-service-theme-selections-v1";
var THEME_SESSION_KEY = "proposal-bot-full-service-current-theme-v1";
var SAVED_TEXT_LIBRARY_KEY = "proposal-bot-saved-section-text-library-v1";
var DELIVERABLE_LIBRARY_KEY = "proposal-bot-deliverable-library-v1";
var CUSTOM_TEMPLATE_KEY = "proposal-bot-custom-proposal-types-v1";
var GENERAL_CONDITIONS_RATE_DEFAULTS_KEY = "proposal-bot-general-conditions-rate-defaults-v1";
var PROCESS_FIXED_HEADING = "Our Design Team";
var PROCESS_SCOPE_FIXED_HEADING = "Scope of Work / Basic Services";
var PHASE_SCOPE_FIXED_HEADING = "In coordination with the AOR, this phase includes the following:";
var PROJECT_SUMMARY_FIXED_HEADING = "Project Summary";
var DEFAULT_GENERAL_CONDITIONS_HOURLY_RATES = [
{ label: "Principal", amount: "375" },
{ label: "Project Manager", amount: "275" },
{ label: "Senior Architect", amount: "250" },
{ label: "Junior Architect", amount: "180" },
{ label: "BIM Support", amount: "160" },
{ label: "Administrative", amount: "110" }
];
var DEFAULT_GENERAL_CONDITIONS_TEXT = [
"1. All work performed by MA will be paid in full and on schedule, regardless of if the project is capitalized or not, the property is acquired or not, and regardless of if the project moves forward or not.",
"2. If there is a delay of more than 180 calendar days between the delivery of the Conceptual Design and the notice to proceed with the rest of the scope, fees may need to be adjusted.",
"3. Fees for Additional Services will be billed on an hourly basis according to the following hourly billing rates:",
"",
"Additional Service fees will increase 10% annually each during the duration of SD, DD and CD. Add Service fees will remain fixed for the duration of the CA phase, at the updated rate charged when CA starts.",
"4. All payments are due 30 days upon receipt of invoice. Payments due to MA and unpaid under this agreement shall bear interest at the rate of 1% per month beginning 60 days from the date of invoice. Payments due to M+A and unpaid under this agreement shall be cause for suspension of work.",
"5. MA regards any services beyond the Basic Architectural Services as additional services, of which the following are examples:",
"- Detailed initial programming",
"- Revisions when inconsistent with approvals or instructions previously given.",
"- As-built drawings",
"- Graphics and signage design",
"- Lighting design",
"- LEED Certification",
"- Extensive community-city hall presentations and negotiations beyond three sessions.",
"- Program changes beyond 75% SD and during and after Design Development",
"- Coordination of commercial renders beyond what is listed above.",
"- Coordination of physical model, if required",
"- Other scope not included in the proposal",
"6. Owner Provided Services",
"The Owner will provide the following owner services:",
"- Boundary, Tree, Utility and Topographic Survey",
"- Geotechnical Investigation and Report",
"- Environmental Assessment / Survey and Remediation",
"- All Consultants, including but not limited to: Architect of Record, Structural, MEP, Interior Design, Civil, Preconstruction Services, Building Envelope, Lighting, Landscape, Graphic, Acoustical, Lighting, etc.",
"7. Reimbursable Expenses",
"MA regards the following as reimbursable expenses, which will be charged with a 1.1 administrative multiplier:",
"- Travel and transportation as required by project duties.",
"- Communication, deliveries, postage and handling",
"- All reprographic expenses including plotting, photocopy copies and faxes",
"- Renderings, models or mockups requested by the Owner.",
"8. Fees assume current MA insurance limits. If additional insurance is requested, Owner will need to pay the difference in premiums, assuming additional insurance is available.",
"9. Project Restarting",
"Excluding the Conceptual Design phase, in case the project is stopped for a continuous period of sixty (60) natural days or more, M+A will have the right to charge a restarting fee and a period of two weeks to reassemble the project team. Before restarting, all fees for previously executed work need to be paid.",
"10. Fees do not include any consultants and do not include design of any leasable areas, amenity and common area interiors, etc.",
"11. Design will be documented in Revit without additional charges."
].join("\n\n");
var DEFAULT_COMPENSATION_FEE_NOTES_TEXT = [
"1. Please note that no fees for consultants are included in this proposal. M+A Fees only.",
"2. Fees are valid for 90 days.",
"3. Fees will be billed monthly according to advance of work.",
"4. Fees include CA site visits twice monthly for 16 months. In case construction lasts longer or more visits are required, additional fees will need to be negotiated.",
"5. The above fees apply when MA is commissioned the full design architect role for SD, DD, CD and CA scopes.",
"6. If after Schematic Design the project is continued by another architect, a disengagement fee will be due. This fee will be 20% of the Conceptual and Schematic Design fees.",
"7. If the program increases by more than 5%, fees may require adjustments.",
"8. Depending on the magnitude, additional changes during DD may require additional fees."
].join("\n");
var DEFAULT_COMPENSATION_FEE_NOTES_REFERENCE = "Please see the General Conditions page for the terms of this proposal.";
var PHOTO_DB_NAME = "proposal-bot-photo-library-v1";
var PHOTO_STORE_NAME = "photos";
var PHOTO_PAN_SCALE = 1.1;
var activeTab = "info";
var activeSectionId = "intro";
var isDirty = false;
var photoSelectionsChanged = false;
var autoSaveTimer = null;
var sharedDrafts = Object.create(null);
var sharedRevisions = Object.create(null);
var sharedSaveChains = {};
var sharedPendingSaves = 0;
var sharedSaveError = "";
var sharedConflictIds = {};
var sharedReady = false;
var projectImageSyncPromise = null;
var photoLibraryTypologyFilter = "all";
var photoThumbnailObserver = null;
var photoThumbnailQueue = [];
var activePhotoThumbnailJobs = 0;
var photoThumbnailGeneration = 0;
var photoThumbnailCache = new Map();
var activePhotoPanKey = "";
var photoPanSession = null;
var photoLibraryDragSession = null;
var themes = [
{
id: "maOg",
label: "M+A OG",
note: "The original portrait proposal layout."
},
{
id: "modernLandscape",
label: "Modern Landscape",
note: "A wide, editorial layout inspired by architectural presentation boards."
},
{
id: "northlinePortrait",
label: "Modern Portrait",
note: "A tall, image-forward proposal style with bold condensed titles and boxed section numbers."
}
];
var templates = [
{
id: "fullService",
label: "Full Service | Office/Retail",
note: "Full architectural design proposal based on the Full Service Example PDF."
},
{
id: "marketingPackage",
label: "Marketing Package",
note: "Placeholder template for a future marketing-scope proposal."
},
{
id: "schematicOnly",
label: "Schematic Only",
note: "Placeholder template for a future schematic-design-only proposal."
},
{
id: "asr",
label: "ASR",
note: "Project Summary, Design Schedule, Compensation, Thank You, and Signature only.",
defaultsSaved: true,
sectionInclusion: {
projectSummary: true,
ourQualifications: false,
ourTeam: false,
process: false,
conceptDesignPage: false,
schematicDesignPage: false,
designDevelopmentPage: false,
constructionDocumentsPage: false,
biddingConstructionAdministration: false,
designSchedulePage: true,
consultantsPage: false,
compensationPage: true,
proceedingWithConfidence: true,
signature: true,
generalConditionsPage: false,
attachmentsPage: false
},
sectionOverrides: {},
sectionTitleOverrides: {},
phaseLabels: {}
}
];
function availableTemplates() {
return templates.concat(readCustomTemplates());
}
function readCustomTemplates() {
try {
var stored = JSON.parse(localStorage.getItem(CUSTOM_TEMPLATE_KEY) || "[]");
if (!Array.isArray(stored)) {
return [];
}
return stored.map(normalizeCustomTemplate).filter(function (item) {
return item.id && item.label;
});
} catch (error) {
return [];
}
}
function normalizeCustomTemplate(item) {
var source = item && typeof item === "object" ? item : {};
var hasDefaults = source.defaultsSaved === true ||
(source.sectionInclusion && typeof source.sectionInclusion === "object") ||
(source.sectionOverrides && typeof source.sectionOverrides === "object") ||
(source.sectionTitleOverrides && typeof source.sectionTitleOverrides === "object") ||
(source.phaseLabels && typeof source.phaseLabels === "object");
return {
id: String(source.id || "").trim(),
label: String(source.label || "").trim(),
note: String(source.note || "Custom proposal type"),
defaultsSaved: !!hasDefaults,
sectionInclusion: hasDefaults ? normalizeSectionInclusion(source.sectionInclusion) : null,
sectionOverrides: hasDefaults ? normalizeProposalTypeText(source.sectionOverrides) : {},
sectionTitleOverrides: hasDefaults ? normalizeSectionTitleOverrides(source.sectionTitleOverrides) : {},
phaseLabels: hasDefaults ? normalizePhaseLabels(source.phaseLabels) : {}
};
}
function normalizeProposalTypeText(value) {
var source = value && typeof value === "object" ? value : {};
var normalized = {};
sectionDefinitions.forEach(function (section) {
if (Object.prototype.hasOwnProperty.call(source, section.id)) {
normalized[section.id] = section.id === "process"
? stripProcessFixedHeading(source[section.id])
: String(source[section.id] || "");
}
});
return normalized;
}
function writeCustomTemplates(list) {
try {
localStorage.setItem(CUSTOM_TEMPLATE_KEY, JSON.stringify(list || []));
return true;
} catch (error) {
console.error("Could not save custom proposal types", error);
return false;
}
}
function createTemplateId(label) {
var base = String(label || "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") || ("proposal-type-" + Date.now());
var existing = availableTemplates().map(function (item) {
return item.id;
});
var candidate = base;
var suffix = 2;
while (existing.indexOf(candidate) >= 0) {
candidate = base + "-" + suffix;
suffix += 1;
}
return candidate;
}
function findTemplateById(templateId) {
return availableTemplates().find(function (item) {
return item.id === templateId;
}) || null;
}
function isCustomTemplateId(templateId) {
return readCustomTemplates().some(function (item) {
return item.id === templateId;
});
}
function captureProposalTypeDefaults(id, label, note) {
var textDefaults = {};
sectionDefinitions.forEach(function (section) {
textDefaults[section.id] = String(getSectionText(section) || "");
});
return {
id: id,
label: label,
note: note || "Saved Subject selections and default section text",
defaultsSaved: true,
sectionInclusion: normalizeSectionInclusion(proposal.sectionInclusion),
sectionOverrides: textDefaults,
sectionTitleOverrides: normalizeSectionTitleOverrides(proposal.sectionTitleOverrides),
phaseLabels: normalizePhaseLabels(proposal.phaseLabels)
};
}
function applyProposalType(template) {
if (!template) {
return;
}
proposal.templateId = template.id;
if (template.defaultsSaved) {
proposal.sectionInclusion = normalizeSectionInclusion(template.sectionInclusion);
proposal.sectionOverrides = normalizeProposalTypeText(template.sectionOverrides);
proposal.sectionTitleOverrides = normalizeSectionTitleOverrides(template.sectionTitleOverrides);
proposal.phaseLabels = normalizePhaseLabels(template.phaseLabels);
delete proposal.projectSummarySections;
}
markDirty();
saveCurrentDraft();
renderAll();
}
function mergeCustomTemplateLists(target, source) {
var merged = {};
(Array.isArray(target) ? target : []).concat(Array.isArray(source) ? source : []).forEach(function (item) {
var normalized = normalizeCustomTemplate(item);
if (normalized.id && normalized.label) {
merged[normalized.id] = normalized;
}
});
return Object.keys(merged).map(function (id) {
return merged[id];
});
}
var defaultPhotoTypologies = ["Unassigned", "MF Low", "MF High", "Office", "Retail", "Hospitality", "Master Plan"];
var defaultPhaseDeliverables = {
conceptDesign: [
"Context plan",
"Ground level Site Plan",
"Typical building plan",
"Building sections",
"Elevations w/ preliminary materials",
"Massing diagrams / White Box 3Ds",
"Area charts",
"In-House renderings"
],
schematicDesign: [
"Architectural Site Plan",
"Programmatic and Area Analysis Sheets",
"Code Analysis Sheets (by AOR)",
"Building Plans including ground, amenity and typical floors",
"Main Exterior Elevations w/ preliminary materials",
"Roof Plans",
"Building Sections",
"Study 3D Renders",
"Preliminary BOMA calculations, by AOR"
],
designDevelopment: [
"Architectural Site Plan",
"Programmatic and Area Analysis Sheets",
"Code Analysis Sheets, by AOR",
"BOMA Calculations, by AOR",
"Building Plans including ground, amenities, and typical floors.",
"Coordination with structural drawings identifying the structural systems for the project",
"Coordination with MEP drawings identifying the HVAC, electrical and plumbing systems",
"Exterior Elevations",
"Lobby Plans and Key Elevations",
"Restroom Plans and Key Elevations",
"Coordination of Amenity Plans (if applicable)",
"Roof Plans",
"Building Sections",
"Wall sections. With AOR",
"Outline Material and Finish Schedules",
"LEED and Environmental coordination.",
"Study 3D Renders",
"Coordination of up to 4 (four) commercial renders (if selected)"
]
};
var sectionDefinitions = [
{ id: "intro", title: "Intro", source: "written", photoType: "context", generator: generateIntro },
{ id: "projectDescription", title: "Project Description", source: "written", photoType: "context", generator: generateProjectDescription },
{ id: "basicOwnerRequirements", title: "Basic owner requirements", source: "written-list", photoType: "program", generator: generateBasicOwnerRequirements },
{ id: "ownerProvidedInfo", title: "Owner provided info", source: "written-list", photoType: "documents", generator: generateOwnerProvidedInfo },
{ id: "ourDesign", title: "Our Design", source: "pdf", photoType: "design", text: [
"PROCESS:",
"",
"Our Design Team",
"",
"One of the firm’s principals is always in charge of our projects, ensuring that our clients and projects always have top level of attention that is required.",
"",
"Our firm has a good understanding of the multifamily developer’s concerns regarding good design, budgets, etc. Within these parameters, our experience allows us to bring innovative solutions not only to the overall design of the building, but also to our approach to unit design. We have the knowhow and expertise to design the next generation of multifamily buildings within your framework and cost.",
"",
"For this project, we will serve as Design Architect with an Architect of Record (AOR) of your choice. As referenced in this RFP we are including AOR fee projections from House Robertson Architects.",
"",
"As Design Architects for this project, we will coordinate closely with the AOR as well as with the other participating consultants. It is our understanding that a General Contractor with whom the intent is to negotiate a construction contract will be selected for pre-design services, including detailed pricing information that will be made available to the Design Team at the milestones defined in the Project Schedule."
].join("\n") },
{ id: "scopeBasicServices", title: "Scope of Work / Basic Services", source: "pdf", photoType: "scope", text: [
"This proposal is made with Munoz + Albin Architecture & Planning Inc. (M+A) as the Design Architect. No AOR or other consultants are included in this proposal. MA and the other consultants will work under contract with the AOR.",
"",
"Prior to starting the design process, in conjunction with MA, the Owner will define any issues such as percentage of balconies desired, as well as types of interior solutions preferred such as laundry, kitchen, bathroom types and number of fixtures, etc. M+A scope includes unit design: layouts, RCPs, typical bathroom and kitchen design. Interior design of public spaces will be provided by the interior designer."
].join("\n") },
{ id: "maAssumptions", title: "MA Assumptions", source: "written-list", photoType: "program", generator: generateAssumptions },
{ id: "conceptDesign", title: "Concept Design", source: "pdf", photoType: "concept", text: [
"Conceptual Design",
"",
"In coordination with the AOR, our pre-design phase focuses on site planning, zoning and code analysis, program development, access and circulation concepts, preliminary consultant coordination, and alignment with project budget and cost expectations.",
"",
"MA will study various site test fit options for the program. Corresponding massing options will be studied and evaluated. At the end of the Conceptual Design scope, an overall scheme will be generated which will be the basis of design.",
"",
"Deliverables for this phase include:",
"• Context plan",
"• Ground level Site Plan",
"• Typical building plan with units shown as blocks. No interior unit layout included at this phase",
"• Typical parking level plan",
"• Elevations of 2 main facades indicating materials",
"• Building sections",
"• Massing diagrams / White Box 3Ds",
"• In-house renders",
"• Area charts"
].join("\n") },
{ id: "schematicDesign", title: "Schematic Design", source: "pdf", photoType: "schematic", text: [
"Schematic Design",
"",
"Based on the Owner’s approval of the concept phase, MA will produce documents to complete the Schematic Design scope. MA will further develop massing and façade solutions that will serve as the basis of design for the building.",
"",
"Schematic Design studies will consist of the drawings and documents listed below:",
"",
"In coordination with the AOR, this phase includes the following:",
"• Site and area analysis to confirm design parameters.",
"• Coordination with local planning, building code, life safety and zoning requirements.",
"• Parking, loading and service requirements.",
"• Building core, elevator and circulation analysis.",
"• Exterior skin and design systems and materials.",
"• Coordination with the civil engineer and other consultants.",
"• Evaluation of structural and MEP building systems.",
"• Presentations and meetings by-weekly with consultants and client",
"• Meetings with the contractor to establish construction budgets for design.",
"• Landscape and interior design coordination.",
"• Program and unit plan development.",
"",
"Deliverables for this phase include:",
"• Architectural Site Plan",
"• Programmatic and Area Analysis Sheets",
"• Code Analysis Sheets (by AOR)",
"• Building Plans including ground, parking, amenity and typical floors",
"• Unit Plans (6 unit types)",
"• Main Exterior Elevations with Materials noted",
"• Roof Plans",
"• Building Sections",
"• Illustrated 3D vignettes of key details",
"• In-house Renders"
].join("\n") },
{ id: "designDevelopment", title: "Design Development", source: "pdf", photoType: "design", text: [
"Design Development",
"",
"Our design development phase is based on the approved schematic design documents and approved preliminary budget by the General Contractor. This phase includes identification of structural, mechanical and electrical systems for pricing purposes.",
"",
"In coordination with the AOR, this phase includes the following:",
"• Site and area analysis to confirm design parameters.",
"• Coordination with local planning, building code, life safety and zoning requirements.",
"• Parking, loading and service requirements.",
"• Building core, elevator and circulation analysis.",
"• Exterior skin and design systems and materials.",
"• Coordination with consultants: Civil, Structural, MEP, Landscape, Interior Designers, etc.",
"• Presentations and OAC meetings as required.",
"• Specialty consultant engagement including waterproofing, accessibility, façade cleaning and maintenance, hardware, elevator, low voltage and others. By AOR.",
"• Continued program and unit plan development.",
"• Plans, elevations, sections and other drawings as necessary to illustrate the final design of the buildings.",
"• Meetings and coordination with the contractor to ensure the design is within budget as the design is developed.",
"",
"Deliverables for this phase include:",
"• Architectural Site Plan",
"• Programmatic and Area Analysis Sheets",
"• Code Analysis Sheets. By AOR",
"• Building Plans including ground, parking, amenities, and typical floors.",
"• Coordination with structural drawings identifying the structural systems for the project",
"• Coordination with MEP drawings identifying the HVAC, electrical and plumbing spatial requirements",
"• All Unit Plans and mix including unit RCP’s",
"• Design of 6 typical units’ kitchens and bathrooms",
"• Exterior Elevations with Materials",
"• Coordination of Amenity Plans (by Interior Designer)",
"• Wind tunnel analysis may be required at this stage.",
"• Roof Plans",
"• Building Sections",
"• Schematic Wall Sections",
"• Illustrated 3D vignettes with plans, sections, elevations and 3D elements",
"• Outline Material and Finish Schedules",
"• LEED and Environmental coordination (by AOR)",
"• Façade cleaning and maintenance coordination",
"• In-house 3D Renders",
"• Coordination of up to 6 (six) commercial renders"
].join("\n") },
{ id: "constructionDocuments", title: "Construction Documents", source: "pdf", photoType: "documents", text: "Based on the Owner’s approvals of the Design Development documents and of the updated preliminary pricing package (by general contractor), M+A will assist the AOR with architectural design issues that need to be addressed during this phase. M+A will assist the AOR in setting forth in detail the quality level of the materials and systems and other requirements for the construction of the building. Permitting will be done by the AOR." },
{ id: "biddingNegotiation", title: "Bidding and Negotiation", source: "pdf", photoType: "documents", text: "M+A understands that the Owner will negotiate the construction contract with a general contractor that will provide pre-construction services including pricing information during the various phases of the project development." },
{ id: "constructionAdministration", title: "Construction Administration", source: "pdf", photoType: "construction", text: [
"M+A shall review the contractor submittals such as shop drawings relevant to building design, product data, and samples and visit the construction site once monthly (structure) and twice monthly (finishes) only for the limited purpose of checking for conformance with information given and compliance with the design concept and aesthetic intent of the project.",
"",
"This phase includes the following:",
"• Additional supplementary or clarification drawings as necessary.",
"• In-house review and approval of shop drawings for compliance with aesthetic intent.",
"• Response to RFI’s from the field.",
"• Submittal review and approval."
].join("\n") },
{ id: "designSchedule", title: "Design Schedule", source: "editable-data", photoType: "schedule", generator: generateDesignSchedule },
{ id: "consultants", title: "Consultants", source: "editable-data", photoType: "consultants", generator: generateConsultants },
{ id: "compensation", title: "Compensation", source: "editable-data", photoType: "fees", generator: generateCompensation },
{ id: "feeSchedule", title: "Fee Schedule", source: "uploaded", photoType: "fees", generator: generateFeeSchedule },
{ id: "feeNotes", title: "Fee Notes", source: "pdf", photoType: "fees", text: [
"1. Fees to be billed monthly according to project advancement.",
"2. A retainer of $20,000.00 upon commission to be amortized through schematic design.",
"3. Please note that no fees for consultants are included in this proposal.",
"4. Fees are valid for 90 days.",
"5. Fees include CA site visits monthly during construction of structure and twice a month during selection of finishes for 36 months. In case construction lasts longer or more visits are required, additional fees will need to be negotiated.",
"6. The above fees apply when MA is commissioned to complete the full design architect role for SD, DD, CD and CA scopes.",
"7. If the program increases by more than 5%, fees may require adjustments.",
"8. Depending on the magnitude, additional changes during DD may require additional fees."
].join("\n") },
{ id: "ending", title: "Ending paragraph", source: "written", photoType: "context", generator: generateEnding },
{ id: "attachments", title: "Attachments", source: "placeholder", photoType: "documents", generator: generateAttachments }
];
var proposalPageGroups = [
{ photoType: "context", sections: ["intro", "projectDescription"] },
{ photoType: "program", sections: ["basicOwnerRequirements", "ownerProvidedInfo"] },
{ photoType: "design", sections: ["ourDesign"] },
{ photoType: "scope", sections: ["scopeBasicServices", "maAssumptions"] },
{ photoType: "concept", sections: ["conceptDesign"] },
{ photoType: "schematic", sections: [{ id: "schematicDesign", part: "scope" }] },
{ photoType: "schematic", sections: [{ id: "schematicDesign", part: "deliverables" }, { id: "designDevelopment", part: "scopeIntro" }] },
{ photoType: "design", sections: [{ id: "designDevelopment", part: "scopeList" }] },
{ photoType: "design", sections: [{ id: "designDevelopment", part: "deliverablesA" }] },
{ photoType: "design", sections: [{ id: "designDevelopment", part: "deliverablesB" }] },
{ photoType: "construction", sections: ["constructionDocuments", "biddingNegotiation", "constructionAdministration"] },
{ photoType: "schedule", sections: ["designSchedule"] },
{ photoType: "consultants", sections: ["consultants"] },
{ photoType: "fees", sections: ["compensation", "feeSchedule", "feeNotes"] },
{ photoType: "documents", sections: ["ending", "attachments"] }
];
var scheduleFieldDefs = [
{ path: "schedule.concept", label: "Concept Design", aliases: ["concept design", "conceptual design", "concept", "pre design", "pre-design", "project kickoff", "kickoff", "programming"] },
{ path: "schedule.schematic", label: "Schematic Design", aliases: ["schematic design", "schematic", "sd"] },
{ path: "schedule.designDevelopment", label: "Design Development", aliases: ["design development", "dd"] },
{ path: "schedule.valueEngineeringStudy", label: "Value Engineering Study", aliases: ["value engineering study", "value engineering", "ve study", "ve"] },
{ path: "schedule.constructionDocuments", label: "Construction Documents", aliases: ["construction documents", "construction document", "cds", "cd"] },
{ path: "schedule.bidding", label: "Bidding", aliases: ["bidding and negotiation", "bidding negotiation", "bidding", "negotiation", "bidding permitting", "permitting"] },
{ path: "schedule.permitIfc", label: "Permit and IFC", aliases: ["permit and ifc", "permit ifc", "permit", "ifc", "permit set", "issue for construction"] },
{ path: "schedule.constructionAdministration", label: "Construction Administration", aliases: ["construction administration", "construction admin", "ca", "construction observation", "ongoing", "through closeout", "closeout"] }
];
var compensationFieldDefs = [
{ path: "compensation.amount", label: "Total fee", aliases: ["total fee", "total", "lump sum", "fee"] },
{ path: "compensation.concept", label: "Conceptual Design", aliases: ["conceptual design", "concept design", "concept"] },
{ path: "compensation.schematic", label: "Schematic Design", aliases: ["schematic design", "schematic", "sd"] },
{ path: "compensation.designDevelopment", label: "Design Development", aliases: ["design development", "dd"] },
{ path: "compensation.constructionDocuments", label: "Construction Documents", aliases: ["construction documents", "construction document", "cds", "cd"] },
{ path: "compensation.bid", label: "Bid", aliases: ["bid", "bidding", "bidding and negotiation", "negotiation"] },
{ path: "compensation.constructionObservation", label: "Construction Administration", aliases: ["construction administration", "construction admin", "construction observation", "cons obs", "cons.obs", "ca"] }
];
var els = {};
var proposal = createDefaultProposal();
document.addEventListener("DOMContentLoaded", async function () {
els.nav = document.getElementById("section-nav");
els.editor = document.getElementById("editor-panel");
els.preview = document.getElementById("proposal-preview");
els.heading = document.getElementById("proposal-heading");
els.status = document.getElementById("draft-status");
els.draftSelect = document.getElementById("draft-select");
els.renameDraft = document.getElementById("rename-draft");
els.deleteDraft = document.getElementById("delete-draft");
els.refreshDrafts = document.getElementById("refresh-drafts");
els.importBackupInput = document.getElementById("import-backup-input");
els.appShell = document.querySelector(".app-shell");
els.workspace = document.querySelector(".workspace");
els.tabs = Array.from(document.querySelectorAll(".tab"));
els.status.textContent = "Loading shared projects...";
try {
await loadSharedDrafts();
await migrateBrowserDrafts();
} catch (error) {
console.error("Could not load shared projects", error);
els.status.textContent = "Shared projects unavailable";
window.alert("Shared projects could not be loaded. Check the server or shared data folder, then reload this page.");
return;
}
sharedReady = true;
bindActions();
loadInitialDraft();
hydrateProposalPhotos(function () {
syncProjectImageFolder(function () {
renderAll();
});
});
});
function bindActions() {
window.addEventListener("beforeunload", function () {
if (isDirty) saveCurrentDraft();
});
window.addEventListener("pagehide", function () {
if (isDirty) saveCurrentDraft();
});
document.addEventListener("visibilitychange", function () {
if (document.visibilityState === "hidden") {
if (isDirty) saveCurrentDraft();
}
});
els.refreshDrafts.addEventListener("click", refreshSharedDrafts);
window.addEventListener("focus", function () {
if (!isDirty && !sharedPendingSaves) refreshSharedDrafts();
});
document.getElementById("new-proposal").addEventListener("click", async function () {
var currentId = proposal.meta.id;
if (isDirty) saveCurrentDraft();
if (sharedSaveChains[currentId]) await sharedSaveChains[currentId];
if (sharedSaveError) {
window.alert("The current project has not been saved to the shared folder. Retry Save before starting a new project.");
return;
}
proposal = createDefaultProposal();
proposal.savedSectionInputs = migrateSavedTextFromDrafts();
activeTab = "info";
activeSectionId = defaultActiveSectionId();
isDirty = true;
saveCurrentDraft();
syncProjectImageFolder(function () {
renderAll();
});
});
document.getElementById("save-proposal").addEventListener("click", function () {
saveCurrentDraft();
renderDraftSelect();
});
document.getElementById("export-json").addEventListener("click", function () {
saveCurrentDraft();
downloadText(filenameBase() + "-data.json", JSON.stringify(proposal, null, 2), "application/json");
});
document.getElementById("export-pdf").addEventListener("click", async function () {
saveCurrentDraft();
await printProposalPdf();
});
document.getElementById("export-word").addEventListener("click", function () {
saveCurrentDraft();
downloadBlob(filenameBase() + "-word.doc", buildWordHtmlBlob(), "application/msword");
});
document.getElementById("export-indesign").addEventListener("click", function () {
saveCurrentDraft();
exportInDesignFile();
});
document.getElementById("backup-app").addEventListener("click", function () {
saveCurrentDraft();
backupAppData();
});
document.getElementById("import-backup").addEventListener("click", function () {
openBackupImport();
});
if (els.importBackupInput) {
els.importBackupInput.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (file) {
importBackupFromFile(file);
}
event.target.value = "";
});
}
document.getElementById("toggle-preview").addEventListener("click", function (event) {
var isWide = els.workspace.classList.toggle("preview-wide");
els.appShell.classList.toggle("preview-wide-shell", isWide);
event.currentTarget.textContent = isWide ? "Editor" : "Wide";
});
els.tabs.forEach(function (button) {
button.addEventListener("click", function () {
activeTab = button.dataset.tab;
saveCurrentDraft();
renderAll();
});
});
els.draftSelect.addEventListener("change", async function (event) {
var selectedId = event.target.value;
var currentId = proposal.meta.id;
if (isDirty) saveCurrentDraft();
if (sharedSaveChains[currentId]) await sharedSaveChains[currentId];
if (sharedSaveError) {
event.target.value = currentId;
window.alert("The current project has not been saved to the shared folder. Retry Save before opening another project.");
return;
}
var drafts = readDrafts();
if (drafts[selectedId]) {
proposal = normalizeProposal(drafts[selectedId]);
restoreRememberedTheme();
proposal.savedSectionInputs = migrateSavedTextFromDrafts();
try { localStorage.setItem(LAST_DRAFT_KEY, proposal.meta.id); } catch (error) {}
activeTab = proposal.meta.activeTab || "info";
activeSectionId = validSectionId(proposal.meta.activeSectionId) ? proposal.meta.activeSectionId : defaultActiveSectionId();
isDirty = false;
hydrateProposalPhotos(function () {
syncProjectImageFolder(function () {
renderAll();
});
});
}
});
els.renameDraft.addEventListener("click", renameSelectedDraft);
els.deleteDraft.addEventListener("click", deleteSelectedDraft);
els.preview.addEventListener("click", function (event) {
var button = event.target.closest("[data-photo-action]");
if (!button) {
return;
}
event.preventDefault();
event.stopPropagation();
if (button.dataset.photoAction === "hand") {
togglePreviewPhotoPan(button.dataset.photoKey, button.closest(".editable-photo-slot"));
return;
}
adjustPreviewPhoto(button.dataset.photoKey, button.dataset.photoType, button.dataset.photoAction);
});
els.preview.addEventListener("change", function (event) {
var select = event.target.closest("[data-photo-choice]");
if (!select) {
return;
}
event.preventDefault();
event.stopPropagation();
choosePreviewPhoto(select.dataset.photoKey, select.dataset.photoType, select.value);
});
els.editor.addEventListener("pointerdown", beginPhotoLibraryDrag);
document.addEventListener("pointermove", movePhotoLibraryDrag);
document.addEventListener("pointerup", endPhotoLibraryDrag);
document.addEventListener("pointercancel", cancelPhotoLibraryDrag);
els.preview.addEventListener("dragover", function (event) {
var slot = event.target.closest(".editable-photo-slot");
if (!slot || !photoSlotControl(slot) || !hasProposalPhotoDrag(event.dataTransfer)) {
return;
}
event.preventDefault();
event.dataTransfer.dropEffect = "copy";
clearPreviewPhotoDropTargets(slot);
slot.classList.add("is-photo-drop-target");
});
els.preview.addEventListener("dragleave", function (event) {
var slot = event.target.closest(".editable-photo-slot");
if (slot && (!event.relatedTarget || !slot.contains(event.relatedTarget))) {
slot.classList.remove("is-photo-drop-target");
}
});
els.preview.addEventListener("drop", function (event) {
var slot = event.target.closest(".editable-photo-slot");
var control = photoSlotControl(slot);
var photoId = proposalPhotoIdFromTransfer(event.dataTransfer);
if (!slot || !control || !photoId) {
return;
}
event.preventDefault();
event.stopPropagation();
clearPreviewPhotoDropState();
choosePreviewPhoto(control.dataset.photoKey, control.dataset.photoType, photoId);
flashStatus("Photo replaced. Choose Hand to reposition it.");
});
els.preview.addEventListener("pointerdown", beginPreviewPhotoPan);
els.preview.addEventListener("pointermove", movePreviewPhotoPan);
els.preview.addEventListener("pointerup", endPreviewPhotoPan);
els.preview.addEventListener("pointercancel", endPreviewPhotoPan);
els.preview.addEventListener("lostpointercapture", endPreviewPhotoPan);
}
async function printProposalPdf() {
flashStatus("Preparing PDF...");
await waitForProposalPrintAssets();
await new Promise(function (resolve) {
requestAnimationFrame(function () {
requestAnimationFrame(resolve);
});
});
window.print();
flashStatus("PDF ready to save.");
}
async function waitForProposalPrintAssets() {
var waits = [];
if (document.fonts && document.fonts.ready) {
waits.push(document.fonts.ready.catch(function () {}));
}
document.querySelectorAll(".proposal-document img").forEach(function (image) {
image.loading = "eager";
if (image.complete) {
if (typeof image.decode === "function") {
waits.push(image.decode().catch(function () {}));
}
return;
}
waits.push(new Promise(function (resolve) {
var timeout = window.setTimeout(resolve, 5000);
function finish() {
window.clearTimeout(timeout);
resolve();
}
image.addEventListener("load", finish, { once: true });
image.addEventListener("error", finish, { once: true });
}));
});
await Promise.all(waits);
}
function createDefaultProposal() {
var today = new Date().toISOString().slice(0, 10);
return normalizeProposal({
meta: {
id: "draft-" + Date.now(),
createdAt: today,
updatedAt: today,
samplePhotosSeeded: true
},
customCover: {
enabled: false,
name: "",
dataUrl: "",
stored: false,
orientation: ""
},
customCompensation: {
enabled: false,
name: "",
dataUrl: "",
stored: false,
orientation: ""
},
templateId: "fullService",
themeId: "maOg",
project: {
proposalDate: today,
projectName: "S. Post Oak Multifamily Development",
projectLocation: "S. Post Oak Lane and S. Wynden Drive, Houston",
clientCompany: "BuildPro Construction / Captiva Development",
clientContact: "Mr. Richard Osika",
clientTitle: "Managing Member",
clientAddress: "299 Market St.\nSaddle Brook, NJ 07663",
program: "High-rise multifamily residential development with podium configuration, approximately 300 residential units, structured parking, and interior and exterior amenity spaces.",
scopeOfWork: "Architectural Design services for a landmark multifamily residential tower, including design architect services through concept, schematic design, design development, construction documents support, bidding coordination, and construction administration.",
siteDescription: "approximately 2-acre site located at the intersection of S. Post Oak Lane and S. Wynden Drive",
designIntent: "timeless architectural character, a strong amenity-driven experience, and a cohesive residential environment that supports a range of lifestyles while establishing a clear identity within the neighborhood",
ownerRequirements: [
"Multifamily residential tower with podium configuration",
"300 total residential units",
"Average unit size of 1,400 square feet",
"Primary focus on one and two-bedroom units, with select three-bedroom units",
"Parking ratio of approximately 1.8 cars per unit with structured parking garage sized at approximately 425 square feet per car",
"Approximately 40 stories in height",
"Strong emphasis on interior and exterior amenity spaces. Interior amenities of approximately 20,000 SF",
"Exterior amenity area of 15,000 square feet",
"Architectural character preference toward a timeless design approach"
].join("\n"),
ownerProvidedInfo: [
"Targeted demographics",
"Targeted Unit Mix",
"Targeted average Unit size",
"Target approximation of Residential gross SF",
"Target gross SF of Amenity Space, interior and exterior",
"Target range of Construction Budget"
].join("\n"),
summaryDeliverables: "",
showSummaryDeliverables: false,
maAssumptions: [
"300 Units at 1,400 SF average unit size",
"420,000 NRA",
"No Retail",
"5,000 SF for lobby",
"20,000 SF of interior amenities",
"15,000 SF of exterior amenity deck",
"Parking at 1.8 per unit = 540 cars"
].join("\n"),
photoTypologies: ""
},
schedule: {
concept: "8 weeks",
schematic: "12 weeks",
designDevelopment: "16 weeks",
valueEngineeringStudy: "",
constructionDocuments: "TBD by AOR",
bidding: "TBD by AOR",
permitIfc: "TBD by AOR",
constructionAdministration: "36 months (estimated)"
},
consultants: [
{ name: "Structural", group: "Primary", included: true },
{ name: "MEP", group: "Primary", included: true },
{ name: "Landscape Architecture", group: "Primary", included: true },
{ name: "Interior Design", group: "Primary", included: true },
{ name: "Elevator Consultant", group: "Secondary", included: true },
{ name: "Envelope and Waterproofing", group: "Secondary", included: true },
{ name: "Lighting", group: "Secondary", included: true },
{ name: "Accessibility", group: "Secondary", included: true },
{ name: "Specifications", group: "Secondary", included: true },
{ name: "Door Hardware", group: "Secondary", included: true },
{ name: "Acoustical", group: "Secondary", included: true },
{ name: "Wind Tunnel", group: "Secondary", included: true },
{ name: "LEED and Sustainability", group: "Secondary", included: true },
{ name: "Façade Maintenance and Cleaning", group: "Secondary", included: true },
{ name: "Civil", group: "Owner coordinated", included: true },
{ name: "Graphics and Signage Consultant", group: "Owner coordinated", included: true },
{ name: "Geotech Consultant", group: "Owner coordinated", included: true },
{ name: "Security / AV Consultant", group: "Owner coordinated", included: true },
{ name: "Survey with Topo", group: "Owner coordinated", included: true }
],
compensation: {
amount: "1,379,550.00",
amountWords: "One million, three hundred seventy-nine thousand, five hundred and fifty and 00/100",
concept: "137,955",
schematic: "383,208",
designDevelopment: "551,820",
constructionDocuments: "91,970",
bid: "",
constructionObservation: "214,597",
showFeeNotes: true,
feeNotesText: DEFAULT_COMPENSATION_FEE_NOTES_TEXT,
feeNotesReference: DEFAULT_COMPENSATION_FEE_NOTES_REFERENCE
},
feeSchedule: {
name: "",
dataUrl: ""
},
photoSelections: {},
scheduleImport: {
name: "",
dataUrl: "",
text: ""
},
compensationImport: {
name: "",
dataUrl: "",
text: ""
},
sectionOverrides: {},
photos: defaultPhotos(),
attachments: [
{ title: "Attachment A", note: "Plot Survey placeholder" },
{ title: "Attachment B", note: "Architect of Record Fees placeholder" },
{ title: "Attachment C", note: "General Conditions placeholder" },
{ title: "Attachment D", note: "Proposed Project Schedule placeholder" },
{ title: "Attachment E", note: "Certificate of Insurance placeholder" }
]
});
}
function normalizeProposal(value) {
var base = value || {};
base.meta = base.meta || {};
base.project = base.project || {};
base.schedule = base.schedule || {};
base.scheduleImport = base.scheduleImport || {};
base.compensation = base.compensation || {};
base.compensationImport = base.compensationImport || {};
base.feeSchedule = base.feeSchedule || {};
base.sectionOverrides = base.sectionOverrides || {};
base.photoSelections = base.photoSelections || {};
base.photos = Array.isArray(base.photos) ? base.photos : [];
if (base.photos.some(function (photo) { return photo.sample; })) {
base.meta.samplePhotosSeeded = true;
}
if (!base.meta.samplePhotosSeeded && !base.photos.length) {
base.photos = defaultPhotos();
base.meta.samplePhotosSeeded = true;
}
base.consultants = Array.isArray(base.consultants) ? base.consultants : [];
base.attachments = Array.isArray(base.attachments) ? base.attachments : [];
if (!base.templateId) {
base.templateId = "fullService";
}
base.themeId = normalizeThemeId(base.themeId || base.meta.themeId);
base.meta.themeId = base.themeId;
if (base.project.photoTypologies === "") {
base.project.photoTypologies = defaultPhotoTypologies.slice().join(", ");
}
if (Array.isArray(base.project.photoTypologies)) {
base.project.photoTypologies = base.project.photoTypologies.join(", ");
}
base.project.photoTypologies = parsePhotoTypologies(base.project.photoTypologies).join(", ");
base.photos.forEach(function (photo) {
if (!hasPhotoTypology(photo.typology)) {
photo.typology = defaultPhotoTypologies[0];
}
});
return base;
}
function defaultPhotos() {
var base = "assets/default-photos/";
return [
{ id: "sample-context-01", name: "Full Service sample context 01", dataUrl: base + "context-01.jpg", typology: "context", included: true, sample: true },
{ id: "sample-context-02", name: "Full Service sample context 02", dataUrl: base + "context-02.jpg", typology: "context", included: true, sample: true },
{ id: "sample-program-01", name: "Full Service sample program 01", dataUrl: base + "program-01.jpg", typology: "program", included: true, sample: true },
{ id: "sample-design-01", name: "Full Service sample design 01", dataUrl: base + "design-01.jpg", typology: "design", included: true, sample: true },
{ id: "sample-scope-01", name: "Full Service sample scope 01", dataUrl: base + "scope-01.jpg", typology: "scope", included: true, sample: true },
{ id: "sample-concept-01", name: "Full Service sample concept 01", dataUrl: base + "concept-01.jpg", typology: "concept", included: true, sample: true }
];
}
function renderAll() {
renderTabs();
renderDraftSelect();
renderNav();
renderEditor();
renderPreview();
updateHeader();
}
function renderTabs() {
els.tabs.forEach(function (button) {
button.classList.toggle("active", button.dataset.tab === activeTab);
});
}
function renderNav() {
if (!validSectionId(activeSectionId)) {
activeSectionId = defaultActiveSectionId();
saveCurrentDraft();
}
els.nav.innerHTML = "";
sectionDefinitions.forEach(function (section, index) {
var button = document.createElement("button");
button.type = "button";
button.className = "step-button" + (section.id === activeSectionId ? " active" : "");
button.innerHTML = '' + (index + 1) + '' + escapeHtml(section.title) + "";
button.addEventListener("click", function () {
activeTab = "sections";
activeSectionId = section.id;
saveCurrentDraft();
renderAll();
});
els.nav.appendChild(button);
});
}
function renderEditor() {
if (activeTab !== "photos") {
resetPhotoLibraryThumbnailLoading();
}
if (activeTab === "notes") {
renderNotesPanel();
} else if (activeTab === "photos") {
renderPhotosPanel();
} else if (activeTab === "sections") {
renderSectionsPanel();
} else {
renderInfoPanel();
}
}
function renderInfoPanel() {
els.editor.innerHTML = "";
var templateCard = panelCard("Proposal Type", "Choose the structure for this proposal.");
var templateGrid = document.createElement("div");
templateGrid.className = "template-grid";
templates.forEach(function (template) {
var button = document.createElement("button");
button.type = "button";
button.className = "template-button" + (proposal.templateId === template.id ? " active" : "");
button.innerHTML = "" + escapeHtml(template.label) + "" + escapeHtml(template.note) + "";
button.addEventListener("click", function () {
proposal.templateId = template.id;
markDirty();
saveCurrentDraft();
renderAll();
});
templateGrid.appendChild(button);
});
templateCard.appendChild(templateGrid);
els.editor.appendChild(templateCard);
var themeCard = panelCard("Theme", "Choose the visual layout for this proposal.");
var themeGrid = document.createElement("div");
themeGrid.className = "template-grid theme-grid";
themes.forEach(function (theme) {
var button = document.createElement("button");
button.type = "button";
button.className = "template-button theme-button" + (proposal.themeId === theme.id ? " active" : "");
button.innerHTML = "" + escapeHtml(theme.label) + "" + escapeHtml(theme.note) + "";
button.addEventListener("click", function () {
setProposalTheme(theme.id);
markDirty();
saveCurrentDraft();
renderAll();
});
themeGrid.appendChild(button);
});
themeCard.appendChild(themeGrid);
els.editor.appendChild(themeCard);
var projectInfoCard = fieldsCard("Project Information", [
fieldDef("project.projectName", "Project name"),
fieldDef("project.proposalDate", "Proposal date", "date"),
fieldDef("project.projectLocation", "Project location"),
fieldDef("project.clientCompany", "Client / owner"),
fieldDef("project.clientContact", "Client contact"),
fieldDef("project.clientTitle", "Client title"),
fieldDef("project.clientAddress", "Client address", "textarea", true)
]);
projectInfoCard.querySelector(".field-grid").appendChild(renderProjectPhotoTypologyField());
els.editor.appendChild(projectInfoCard);
renderClientLogosCard();
renderDesignScheduleCard();
renderCompensationCard();
renderConsultantsCard();
renderFeeScheduleCard();
}
function renderProjectPhotoTypologyField() {
var wrap = document.createElement("div");
wrap.className = "field full";
var label = document.createElement("label");
label.textContent = "Photo typologies to include";
wrap.appendChild(label);
var selected = parsePhotoTypologies(proposal.project.photoTypologies);
var options = availablePhotoTypologies();
var visibleSelection = selected.filter(function (item) {
return options.map(normalizeTypology).indexOf(normalizeTypology(item)) >= 0;
});
var list = document.createElement("div");
list.className = "typology-options";
options.forEach(function (option) {
var optionKey = normalizeTypology(option);
var row = document.createElement("label");
row.className = "typology-option";
var checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = visibleSelection.indexOf(optionKey) >= 0;
checkbox.addEventListener("change", function () {
var current = Array.from(list.querySelectorAll(".typology-option input:checked")).map(function (input) {
return input.value;
});
proposal.project.photoTypologies = current.join(", ");
markDirty();
saveCurrentDraft();
renderAll();
});
checkbox.value = optionKey;
row.appendChild(checkbox);
row.appendChild(document.createTextNode(formatTypologyLabel(option)));
list.appendChild(row);
});
wrap.appendChild(list);
return wrap;
}
function renderConsultantsCard() {
var card = panelCard("Consultants", "Toggle consultants in or out for this proposal.");
var list = document.createElement("div");
list.className = "option-list";
proposal.consultants.forEach(function (consultant, index) {
var row = document.createElement("button");
row.type = "button";
row.className = "option-row" + (consultant.included ? " active" : "");
row.innerHTML = "" + escapeHtml(consultant.name) + "" + escapeHtml(consultant.group) + (consultant.included ? " - included" : " - excluded") + "";
row.addEventListener("click", function () {
proposal.consultants[index].included = !proposal.consultants[index].included;
markDirty();
renderAll();
});
list.appendChild(row);
});
card.appendChild(list);
els.editor.appendChild(card);
}
function renderDesignScheduleCard() {
var card = panelCard("Design Schedule", "Type durations manually, or drop/paste a schedule image to read it.");
var grid = document.createElement("div");
grid.className = "field-grid";
scheduleFieldDefs.forEach(function (field) {
grid.appendChild(renderField(field));
});
card.appendChild(grid);
var tools = document.createElement("div");
tools.className = "schedule-import";
var drop = document.createElement("div");
drop.className = "drop-zone schedule-drop-zone";
drop.tabIndex = 0;
drop.innerHTML = '' + (proposal.scheduleImport.name ? escapeHtml(proposal.scheduleImport.name) : "Drop or paste schedule image") + '' + (proposal.scheduleImport.name ? "Reading this image can update the fields above." : "Click this area, then paste an image from your clipboard, or drag one in.") + '';
bindScheduleImageDropZone(drop);
var imageWrap = document.createElement("div");
imageWrap.className = "schedule-image-preview";
if (proposal.scheduleImport.dataUrl) {
imageWrap.innerHTML = '';
} else {
imageWrap.innerHTML = "No schedule image yet";
}
var textWrap = document.createElement("div");
textWrap.className = "schedule-import-text";
var label = document.createElement("label");
label.setAttribute("for", "schedule-import-text");
label.textContent = "Recognized schedule text";
var textarea = document.createElement("textarea");
textarea.id = "schedule-import-text";
textarea.value = proposal.scheduleImport.text || "";
textarea.placeholder = "Recognized schedule text will appear here. You can also paste text from a schedule and apply it.";
textarea.addEventListener("input", function () {
proposal.scheduleImport.text = textarea.value;
markDirty();
});
var actions = document.createElement("div");
actions.className = "schedule-import-actions";
var upload = document.createElement("input");
upload.type = "file";
upload.accept = "image/*";
upload.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (file) {
importScheduleImage(file, drop);
}
});
var apply = document.createElement("button");
apply.type = "button";
apply.className = "secondary compact";
apply.textContent = "Apply Text";
apply.addEventListener("click", function () {
applyScheduleText(textarea.value, "No matching schedule rows were found.");
});
var clear = document.createElement("button");
clear.type = "button";
clear.className = "danger compact";
clear.textContent = "Clear Image";
clear.addEventListener("click", function () {
proposal.scheduleImport = { name: "", dataUrl: "", text: "" };
markDirty();
renderAll();
});
actions.appendChild(upload);
actions.appendChild(apply);
actions.appendChild(clear);
textWrap.appendChild(label);
textWrap.appendChild(textarea);
textWrap.appendChild(actions);
tools.appendChild(drop);
tools.appendChild(imageWrap);
tools.appendChild(textWrap);
card.appendChild(tools);
els.editor.appendChild(card);
}
function renderCompensationCard() {
var card = panelCard("Compensation", "Type amounts manually, or drop/paste a compensation image to read the fee breakdown.");
var grid = document.createElement("div");
grid.className = "field-grid";
[
fieldDef("compensation.amount", "Total fee"),
fieldDef("compensation.amountWords", "Amount in words", "textarea", true),
fieldDef("compensation.concept", "Conceptual Design"),
fieldDef("compensation.schematic", "Schematic Design"),
fieldDef("compensation.designDevelopment", "Design Development"),
fieldDef("compensation.constructionDocuments", "Construction Documents"),
fieldDef("compensation.bid", "Bid"),
fieldDef("compensation.constructionObservation", "Construction Administration")
].forEach(function (field) {
grid.appendChild(renderField(field));
});
card.appendChild(grid);
var tools = document.createElement("div");
tools.className = "schedule-import";
var drop = document.createElement("div");
drop.className = "drop-zone compensation-drop-zone";
drop.tabIndex = 0;
drop.innerHTML = '' + (proposal.compensationImport.name ? escapeHtml(proposal.compensationImport.name) : "Drop or paste compensation image") + '' + (proposal.compensationImport.name ? "Reading this image can update the compensation fields above." : "Click this area, then paste an image from your clipboard, or drag one in.") + '';
bindCompensationImageDropZone(drop);
var imageWrap = document.createElement("div");
imageWrap.className = "schedule-image-preview";
if (proposal.compensationImport.dataUrl) {
imageWrap.innerHTML = '';
} else {
imageWrap.innerHTML = "No compensation image yet";
}
var textWrap = document.createElement("div");
textWrap.className = "schedule-import-text";
var label = document.createElement("label");
label.setAttribute("for", "compensation-import-text");
label.textContent = "Recognized compensation text";
var textarea = document.createElement("textarea");
textarea.id = "compensation-import-text";
textarea.value = proposal.compensationImport.text || "";
textarea.placeholder = "Recognized compensation text will appear here. You can also paste text from a fee image and apply it.";
textarea.addEventListener("input", function () {
proposal.compensationImport.text = textarea.value;
markDirty();
});
var actions = document.createElement("div");
actions.className = "schedule-import-actions";
var upload = document.createElement("input");
upload.type = "file";
upload.accept = "image/*";
upload.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (file) {
importCompensationImage(file, drop);
}
});
var apply = document.createElement("button");
apply.type = "button";
apply.className = "secondary compact";
apply.textContent = "Apply Text";
apply.addEventListener("click", function () {
applyCompensationText(textarea.value, "No matching compensation amounts were found.");
});
var clear = document.createElement("button");
clear.type = "button";
clear.className = "danger compact";
clear.textContent = "Clear Image";
clear.addEventListener("click", function () {
proposal.compensationImport = { name: "", dataUrl: "", text: "" };
markDirty();
renderAll();
});
actions.appendChild(upload);
actions.appendChild(apply);
actions.appendChild(clear);
textWrap.appendChild(label);
textWrap.appendChild(textarea);
textWrap.appendChild(actions);
tools.appendChild(drop);
tools.appendChild(imageWrap);
tools.appendChild(textWrap);
card.appendChild(tools);
els.editor.appendChild(card);
}
function bindScheduleImageDropZone(drop) {
drop.addEventListener("dragover", function (event) {
event.preventDefault();
drop.classList.add("drag-over");
});
drop.addEventListener("dragleave", function () {
drop.classList.remove("drag-over");
});
drop.addEventListener("drop", function (event) {
event.preventDefault();
drop.classList.remove("drag-over");
var file = event.dataTransfer.files && Array.from(event.dataTransfer.files).find(function (item) {
return /^image\//.test(item.type);
});
if (file) {
importScheduleImage(file, drop);
}
});
drop.addEventListener("paste", function (event) {
var items = Array.from((event.clipboardData && event.clipboardData.items) || []);
var imageItem = items.find(function (item) { return /^image\//.test(item.type); });
if (imageItem) {
event.preventDefault();
importScheduleImage(imageItem.getAsFile(), drop);
}
});
drop.addEventListener("click", function () {
drop.focus();
});
}
function bindCompensationImageDropZone(drop) {
drop.addEventListener("dragover", function (event) {
event.preventDefault();
drop.classList.add("drag-over");
});
drop.addEventListener("dragleave", function () {
drop.classList.remove("drag-over");
});
drop.addEventListener("drop", function (event) {
event.preventDefault();
drop.classList.remove("drag-over");
var file = event.dataTransfer.files && Array.from(event.dataTransfer.files).find(function (item) {
return /^image\//.test(item.type);
});
if (file) {
importCompensationImage(file, drop);
}
});
drop.addEventListener("paste", function (event) {
var items = Array.from((event.clipboardData && event.clipboardData.items) || []);
var imageItem = items.find(function (item) { return /^image\//.test(item.type); });
if (imageItem) {
event.preventDefault();
importCompensationImage(imageItem.getAsFile(), drop);
}
});
drop.addEventListener("click", function () {
drop.focus();
});
}
function renderFeeScheduleCard() {
var card = panelCard("Fee Schedule", "Drag and drop a fee schedule image or PDF here.");
var drop = document.createElement("div");
drop.className = "drop-zone";
drop.innerHTML = "" + (proposal.feeSchedule.name ? escapeHtml(proposal.feeSchedule.name) : "Drop fee schedule here") + "" + (proposal.feeSchedule.name ? "This file will appear in the Fee Schedule section." : "Supports images or PDFs stored in this draft.") + "";
drop.addEventListener("dragover", function (event) {
event.preventDefault();
drop.classList.add("drag-over");
});
drop.addEventListener("dragleave", function () {
drop.classList.remove("drag-over");
});
drop.addEventListener("drop", function (event) {
event.preventDefault();
drop.classList.remove("drag-over");
var file = event.dataTransfer.files && event.dataTransfer.files[0];
if (file) {
readFileAsDataUrl(file, function (dataUrl) {
proposal.feeSchedule = { name: file.name, dataUrl: dataUrl };
markDirty();
renderAll();
});
}
});
var input = document.createElement("input");
input.type = "file";
input.accept = "image/*,.pdf";
input.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (file) {
readFileAsDataUrl(file, function (dataUrl) {
proposal.feeSchedule = { name: file.name, dataUrl: dataUrl };
markDirty();
renderAll();
});
}
});
card.appendChild(drop);
card.appendChild(input);
els.editor.appendChild(card);
}
function renderSectionsPanel() {
els.editor.innerHTML = "";
var section = sectionDefinitions.find(function (item) { return item.id === activeSectionId; }) || sectionDefinitions[0];
var generated = getSectionText(section);
var card = document.createElement("section");
card.className = "section-editor";
card.innerHTML = '
";
}).join("");
}
return textToHtmlContinuous(getSectionText(section));
}
function getSectionText(section) {
if (section.virtualText !== undefined) {
return section.virtualText;
}
if (section.id === "generalConditionsPage") {
return normalizeGeneralConditions(proposal.generalConditions).text;
}
if (Object.prototype.hasOwnProperty.call(proposal.sectionOverrides, section.id)) {
return proposal.sectionOverrides[section.id];
}
if (section.generator) {
return section.generator();
}
return generateContextualSectionText(section);
}
function getSectionSourceText(section) {
if (Object.prototype.hasOwnProperty.call(proposal.sectionOverrides, section.id)) {
return proposal.sectionOverrides[section.id];
}
if (section.generator) {
return section.generator();
}
return generateContextualSectionText(section);
}
function generateContextualSectionText(section) {
var c = projectContext();
switch (section.id) {
case "ourDesign":
return [
"PROCESS:",
"",
"Our Design Team",
"",
"One of the firm's principals is always in charge of our projects, ensuring that " + c.clientReference + " and the project team receive the level of attention required for " + c.projectReference + ".",
"",
"Our firm has a strong understanding of the design, budget, schedule, entitlement, and coordination concerns that shape " + c.projectTypePlural + ". Within these parameters, our experience allows us to bring thoughtful solutions not only to the overall character of the building, but also to the organization of the program, arrival sequence, amenity experience, unit planning, and long-term value of the development.",
"",
"For this project, M+A will serve as Design Architect for " + c.projectName + ". We will work with the Owner, the Architect of Record, and the consultant team to establish a clear design direction that responds to " + c.location + ", the stated program, and the project goals described in this proposal.",
"",
"As Design Architects for this project, we will coordinate closely with the AOR as well as with the other participating consultants. It is our understanding that a General Contractor may be selected for pre-design services, including detailed pricing information that will be made available to the Design Team at the milestones defined in the Project Schedule."
].join("\n");
case "scopeBasicServices":
return [
"This proposal is made with Munoz + Albin Architecture & Planning Inc. (M+A) as the Design Architect for " + c.projectName + ". The proposed services are organized around " + c.scopeSummary + ". No AOR or other consultants are included in this proposal unless specifically noted. M+A and the other consultants will work under contract with the AOR or Owner as directed.",
"",
"Prior to starting the design process, in conjunction with M+A, the Owner will confirm the key project parameters including program, target unit mix, desired amenity strategy, parking approach, budget expectations, schedule priorities, and any preferences that may affect the architectural direction. M+A's scope includes design leadership for the overall architectural concept, planning approach, massing, exterior character, amenity planning coordination, typical unit layout strategy, and design coordination through the phases described below."
].join("\n");
case "conceptDesign":
return [
"Conceptual Design",
"",
"In coordination with the AOR, our pre-design phase will focus on site planning, zoning and code analysis, program development, access and circulation concepts, preliminary consultant coordination, and alignment with project budget and cost expectations for " + c.projectName + ".",
"",
"M+A will study site test-fit options for " + c.programSummary + ". Corresponding massing options will be studied and evaluated against the Owner's goals, the project location, the desired design character, and the anticipated construction framework. At the end of the Conceptual Design scope, an overall scheme will be generated which will be the basis of design.",
"",
"Deliverables for this phase include:",
"- Context plan",
"- Ground level site plan",
"- Typical building plan with program areas shown diagrammatically",
"- Typical parking level plan, if applicable",
"- Elevations of two main facades indicating preliminary material direction",
"- Building sections",
"- Massing diagrams / white box 3Ds",
"- In-house renders",
"- Area charts"
].join("\n");
case "schematicDesign":
return [
"Schematic Design",
"",
"Based on the Owner's approval of the concept phase, M+A will produce documents to complete the Schematic Design scope for " + c.projectName + ". M+A will further develop massing, facade direction, planning organization, and design systems that will serve as the basis of design for the project.",
"",
"Schematic Design studies will consist of the drawings and documents listed below:",
"",
"In coordination with the AOR, this phase includes the following:",
"- Site and area analysis to confirm design parameters.",
"- Coordination with local planning, building code, life safety and zoning requirements.",
"- Parking, loading and service requirements, as applicable to the program.",
"- Building core, elevator and circulation analysis.",
"- Exterior skin, design systems, and preliminary materials.",
"- Coordination with civil engineer and other consultants.",
"- Evaluation of structural and MEP building systems.",
"- Presentations and meetings with consultants and client.",
"- Meetings with the contractor to align design direction with construction budget.",
"- Landscape and interior design coordination.",
"- Program and plan development based on the approved concept direction.",
"",
"Deliverables for this phase include:",
"- Architectural site plan",
"- Programmatic and area analysis sheets",
"- Code analysis sheets by AOR",
"- Building plans including ground level, parking, amenity, and typical levels as applicable",
"- Representative unit or program plans",
"- Main exterior elevations with materials noted",
"- Roof plans",
"- Building sections",
"- Illustrated 3D vignettes of key design areas",
"- In-house renders"
].join("\n");
case "designDevelopment":
return [
"Design Development",
"",
"Our design development phase is based on the approved schematic design documents and the updated preliminary budget provided by the General Contractor. This phase includes continued development of the architectural design for " + c.projectName + " and identification of structural, mechanical, electrical, envelope, and material systems for pricing and coordination purposes.",
"",
"In coordination with the AOR, this phase includes the following:",
"- Site and area analysis to confirm design parameters.",
"- Coordination with local planning, building code, life safety and zoning requirements.",
"- Parking, loading and service requirements.",
"- Building core, elevator and circulation analysis.",
"- Exterior skin, design systems, and materials.",
"- Coordination with consultants including civil, structural, MEP, landscape, interior design, and other specialty disciplines.",
"- Presentations and OAC meetings as required.",
"- Specialty consultant engagement including waterproofing, accessibility, facade cleaning and maintenance, hardware, elevator, low voltage, and others by AOR.",
"- Continued program and plan development.",
"- Plans, elevations, sections, and other drawings as necessary to illustrate the final design of the project.",
"- Meetings and coordination with the contractor to confirm that the design direction remains aligned with the project budget as the design is developed.",
"",
"Deliverables for this phase include:",
"- Architectural site plan",
"- Programmatic and area analysis sheets",
"- Code analysis sheets by AOR",
"- Building plans including ground, parking, amenities, and typical floors as applicable",
"- Coordination with structural drawings identifying the structural systems for the project",
"- Coordination with MEP drawings identifying HVAC, electrical, and plumbing spatial requirements",
"- Unit plans, program plans, and mix information as applicable",
"- Typical kitchen, bathroom, or program-area planning studies as required by the project type",
"- Exterior elevations with materials",
"- Coordination of amenity plans by interior designer",
"- Wind tunnel analysis may be required at this stage",
"- Roof plans",
"- Building sections",
"- Schematic wall sections",
"- Illustrated 3D vignettes with plans, sections, elevations, and 3D elements",
"- Outline material and finish schedules",
"- LEED and environmental coordination by AOR",
"- Facade cleaning and maintenance coordination",
"- In-house 3D renders",
"- Coordination of up to six commercial renders"
].join("\n");
case "constructionDocuments":
return "Based on the Owner's approvals of the Design Development documents and updated preliminary pricing package by the General Contractor, M+A will assist the AOR with architectural design issues that need to be addressed during the Construction Documents phase for " + c.projectName + ". M+A will assist the AOR in setting forth the quality level of materials, systems, and other design requirements for the construction of the project. Permitting will be completed by the AOR.";
case "biddingNegotiation":
return "M+A understands that the Owner will negotiate the construction contract with a general contractor that can provide pre-construction services and pricing information during the various phases of project development. M+A will support design-related coordination and clarification as needed so that pricing and scope decisions remain aligned with the approved design intent for " + c.projectName + ".";
case "constructionAdministration":
return [
"M+A shall review contractor submittals such as shop drawings relevant to building design, product data, and samples, and visit the construction site at intervals appropriate to the scope of work for the limited purpose of checking for conformance with information given and compliance with the design concept and aesthetic intent of " + c.projectName + ".",
"",
"This phase includes the following:",
"- Additional supplementary or clarification drawings as necessary.",
"- In-house review and approval of shop drawings for compliance with aesthetic intent.",
"- Responses to RFIs from the field.",
"- Submittal review and approval."
].join("\n");
case "feeNotes":
return [
"1. Fees to be billed monthly according to project advancement.",
"2. A retainer of $20,000.00 upon commission is to be amortized through schematic design unless otherwise agreed.",
"3. Please note that no fees for consultants are included in this proposal unless specifically listed in the consultant section.",
"4. Fees are valid for 90 days.",
"5. Fees include construction administration site visits based on the service assumptions identified for " + c.projectName + ". If construction lasts longer, the program changes materially, or additional visits are required, additional fees will need to be negotiated.",
"6. The above fees apply when M+A is commissioned to complete the design architect role for the scopes described in this proposal.",
"7. If the program, scope, schedule, or level of service increases materially, fees may require adjustment.",
"8. Depending on magnitude, additional changes during design development or later phases may require additional fees."
].join("\n");
default:
return section.text || "";
}
}
function projectContext() {
var p = proposal.project || {};
var program = compactSentence(p.program, "the proposed development");
var scope = compactSentence(p.scopeOfWork, "architectural design services");
var ownerRequirements = [];
var maAssumptions = [];
var projectName = p.projectName || "the project";
var location = p.projectLocation || p.siteDescription || "the project site";
var site = p.siteDescription || location;
var clientCompany = p.clientCompany || "the Owner";
var projectType = inferProjectType(program + " " + projectName);
return {
projectName: projectName,
location: location,
site: site,
clientCompany: clientCompany,
clientReference: clientCompany === "the Owner" ? "the Owner" : clientCompany,
projectReference: projectName === "the project" ? "this project" : projectName,
projectType: projectType,
projectTypePlural: pluralProjectType(projectType),
programSummary: lowerFirst(program),
scopeSummary: lowerFirst(scope),
programNotes: splitProgramNotes(p.program),
ownerRequirements: ownerRequirements,
ownerRequirementSummary: summarizeList(ownerRequirements, "the Owner's stated project requirements"),
maAssumptions: maAssumptions,
maAssumptionSummary: summarizeList(maAssumptions, "the assumptions listed in the Project Information"),
designIntent: inferDesignIntent(program, scope)
};
}
function inferProjectType(text) {
var value = String(text || "").toLowerCase();
if (value.indexOf("multifamily") >= 0 || value.indexOf("residential") >= 0 || value.indexOf("apartment") >= 0) {
return "residential development";
}
if (value.indexOf("hotel") >= 0 || value.indexOf("hospitality") >= 0) {
return "hospitality project";
}
if (value.indexOf("office") >= 0) {
return "commercial office project";
}
if (value.indexOf("retail") >= 0 || value.indexOf("mixed-use") >= 0 || value.indexOf("mixed use") >= 0) {
return "mixed-use project";
}
return "architectural project";
}
function inferDesignIntent(program, scope) {
var text = String(program || "") + " " + String(scope || "");
var value = text.toLowerCase();
if (value.indexOf("office") >= 0 || value.indexOf("retail") >= 0 || value.indexOf("mixed-use") >= 0 || value.indexOf("mixed use") >= 0) {
return "a clear commercial identity, efficient tenant and visitor experience, active ground-level planning, and a coordinated design response to the project context";
}
if (value.indexOf("multifamily") >= 0 || value.indexOf("residential") >= 0 || value.indexOf("apartment") >= 0) {
return "a timeless architectural character, efficient residential planning, a strong amenity experience, and a cohesive environment that supports the Owner's development goals";
}
if (value.indexOf("hotel") >= 0 || value.indexOf("hospitality") >= 0) {
return "a memorable guest experience, clear arrival sequence, efficient back-of-house planning, and an architectural identity suited to the hospitality program";
}
return "a clear architectural identity, strong planning logic, and a coordinated design process";
}
function pluralProjectType(value) {
if (value === "residential development") {
return "residential developments";
}
if (value === "architectural project") {
return "architectural projects";
}
return value + "s";
}
function compactSentence(value, fallback) {
var text = cleanLines(String(value || "").split(/\r?\n/)).join(" ");
return text.replace(/\s+/g, " ").trim() || fallback;
}
function summarizeList(items, fallback) {
var cleaned = cleanLines(items).slice(0, 4);
if (!cleaned.length) {
return fallback;
}
if (cleaned.length === 1) {
return cleaned[0];
}
if (cleaned.length === 2) {
return cleaned[0] + " and " + lowerFirst(cleaned[1]);
}
return cleaned.slice(0, -1).join(", ") + ", and " + lowerFirst(cleaned[cleaned.length - 1]);
}
function splitProgramNotes(value) {
var lines = cleanLines(String(value || "").split(/\r?\n/));
var notes = [];
lines.forEach(function (line) {
String(line || "")
.replace(/\.\s*$/g, "")
.split(/\s*;\s*|\s*,\s*(?:and\s+)?/)
.forEach(function (part) {
var cleaned = part
.replace(/^(?:and|with|including|include|includes)\s+/i, "")
.replace(/\s+/g, " ")
.trim();
if (cleaned) {
notes.push(cleaned);
}
});
});
return uniqueCleanLines(notes);
}
function uniqueCleanLines(items) {
var seen = {};
return cleanLines(items).filter(function (item) {
var key = normalizeTitle(item);
if (!key || seen[key]) {
return false;
}
seen[key] = true;
return true;
});
}
function sentenceCase(value) {
var text = String(value || "").trim();
return text ? text.charAt(0).toUpperCase() + text.slice(1) : text;
}
function articleFor(value) {
return /^[aeiou]/i.test(String(value || "")) ? "an" : "a";
}
function generateIntro() {
var p = proposal.project;
return [
formatDate(p.proposalDate),
"",
cleanLines([p.clientContact, p.clientTitle, p.clientCompany, p.clientAddress]).join("\n"),
"",
"Proposal for Architectural Design services for " + (p.projectName || "the project") + ".",
"",
"Dear " + firstName(p.clientContact || "Client") + ",",
"",
"Thank you for the opportunity to present this proposal for the Architectural Design services for the project described below."
].join("\n");
}
function generateProjectDescription() {
var p = proposal.project;
return [
"The project consists of the planning and design of " + lowerFirst(p.program || "a multifamily residential development") + " on " + (p.siteDescription || "the project site") + ".",
"",
"The development is envisioned as a landmark project that will positively contribute to the surrounding urban context.",
"",
"The overall design approach prioritizes " + (p.designIntent || "a timeless architectural character, a strong amenity-driven experience, and a cohesive environment that supports the Owner’s goals") + "."
].join("\n");
}
function generateBasicOwnerRequirements() {
return "The basic Owner requirements include:\n" + bulletText(proposal.project.ownerRequirements);
}
function generateOwnerProvidedInfo() {
return "The owner will provide the following information and parameters to be followed:\n" + bulletText(proposal.project.ownerProvidedInfo);
}
function generateAssumptions() {
return "For this proposal, M+A has made the following assumptions:\n" + bulletText(proposal.project.maAssumptions);
}
function generateDesignSchedule() {
return includedScheduleRows().map(function (row) {
return row.label + ": " + row.value;
}).concat([
"",
"MA and the AOR will work out a mutually agreeable schedule with the Owner if the above do not correspond with Owner’s expectations. Durations above do not include approval times by Owner.",
"",
"See ATTACHMENT D for Schedule"
]).join("\n");
}
function generateConsultants() {
return consultantGroups().map(function (group) {
return group.name + "\n" + group.items.map(function (item) { return "• " + item.name; }).join("\n");
}).join("\n\n");
}
function generateCompensation() {
return [
"The fees for the Basic Architectural Design Services described above will be a lump sum of $" + proposal.compensation.amount + " (" + proposal.compensation.amountWords + ") billed monthly and calculated in the following manner:",
"",
"Conceptual Design $" + proposal.compensation.concept,
"Schematic Design $" + proposal.compensation.schematic,
"Design Development $" + proposal.compensation.designDevelopment,
"Construction Documents $" + proposal.compensation.constructionDocuments,
"Bid $" + proposal.compensation.bid,
"Construction Administration $" + proposal.compensation.constructionObservation,
"$" + proposal.compensation.amount,
"",
"See ATTACHMENT B for AOR proposed fees."
].join("\n");
}
function generateFeeSchedule() {
return proposal.feeSchedule.name ? "Fee schedule uploaded: " + proposal.feeSchedule.name : "Drag and drop a fee schedule in the Project Info tab. This page will hold the uploaded schedule.";
}
function generateEnding() {
var contact = proposal.project.clientContact || "we";
return [
firstName(contact) + ", we greatly appreciate this opportunity and are looking forward to working with you and your team on this project. Please do not hesitate to call me if you have any questions.",
"",
"Warmest regards,",
"",
"Jorge Munoz, AIA",
"Principal",
"Munoz+Albin, Architecture & Planning Inc."
].join("\n");
}
function generateAttachments() {
return proposal.attachments.map(function (item) {
return [item.title, item.subtitle, item.note].filter(Boolean).join("\n");
}).join("\n\n");
}
function generateIntro() {
var p = proposal.project;
var c = projectContext();
return [
formatDate(p.proposalDate),
"",
cleanLines([p.clientContact, p.clientTitle, p.clientCompany, p.clientAddress]).join("\n"),
"",
"Proposal for Architectural Design services for " + c.projectName + ".",
"",
"Dear " + firstName(p.clientContact || "Client") + ",",
"",
"Thank you for the opportunity to present this proposal for Architectural Design services for the project described below. We understand that " + c.projectName + " requires a clear design vision, disciplined coordination, and a process that keeps the Owner's program, budget, and schedule visible as the work develops."
].join("\n");
}
function generateProjectDescription() {
var c = projectContext();
return [
c.projectName + " is envisioned as " + articleFor(c.projectType) + " " + c.projectType + " located at " + c.location + ". The work described in this proposal is based on " + c.programSummary + ", with the design effort focused on translating the Owner's program, site conditions, and project goals into a clear architectural direction.",
"",
"Based on the information provided, the project priorities include " + lowerFirst(c.ownerRequirementSummary) + ". M+A will use these parameters to guide early planning studies, massing, circulation, exterior character, amenity or program organization, and coordination with the Architect of Record and consultant team as the design develops.",
"",
sentenceCase(c.scopeSummary) + " will be organized around a process that balances design quality, planning efficiency, constructability, consultant coordination, and the Owner's schedule and budget expectations. The overall design approach prioritizes " + c.designIntent + "."
].join("\n");
}
function generateBasicOwnerRequirements() {
return "The basic Owner requirements include:\n" + bulletText(inferredOwnerRequirements().join("\n"));
}
function generateOwnerProvidedInfo() {
var info = [
"Final development program and target unit or program mix",
"Target gross square footage and area priorities",
"Target construction budget and cost expectations",
"Schedule requirements and milestone dates",
"Design standards, brand expectations, or owner preferences",
"Available surveys, reports, and entitlement information"
].join("\n");
return "The owner will provide the following information and parameters to be followed:\n" + bulletText(info);
}
function generateAssumptions() {
return "For this proposal, M+A has made the following assumptions:\n" + bulletText(inferredAssumptions().join("\n"));
}
function inferredOwnerRequirements() {
var c = projectContext();
var requirements = [
"Architectural design services for " + c.projectName,
"Design response to " + c.location + " and the surrounding context",
"Scope of work organized around " + c.scopeSummary,
"Overall design approach prioritizing " + lowerFirst(c.designIntent),
"Planning and design coordination aligned with the Owner's budget and schedule goals",
"Consultant coordination appropriate to the approved scope of work"
];
return uniqueCleanLines(requirements.concat(formattedRequirementItems(c)));
}
function inferredAssumptions() {
return inferredAssumptionsFromProgram(proposal.project && proposal.project.program);
}
function inferredAssumptionsFromProgram(programText) {
var p = proposal.project || {};
var program = compactSentence(programText, "the proposed development");
var projectName = p.projectName || "the project";
var location = p.projectLocation || p.siteDescription || "the project site";
var site = p.siteDescription || location;
var projectType = inferProjectType(program + " " + projectName);
var context = {
site: site,
scopeSummary: lowerFirst(compactSentence(p.scopeOfWork, "architectural design services")),
designIntent: inferDesignIntent(program, compactSentence(p.scopeOfWork, "architectural design services")),
programNotes: splitProgramNotes(program),
programSummary: lowerFirst(program),
projectType: projectType,
projectTypePlural: pluralProjectType(projectType)
};
var assumptions = [
"Project program is based on the information provided by the Owner",
"Site information is based on " + context.site,
"Owner will provide target budget, schedule, and approval milestones",
"AOR and required consultants will provide code, technical, and permitting coordination unless otherwise noted",
"Scope is based on " + context.scopeSummary,
"Design direction will prioritize " + lowerFirst(context.designIntent),
"Material program or schedule changes may require adjustment to fees and services"
];
return uniqueCleanLines(assumptions.concat(formattedAssumptionItems(context)));
}
function formattedRequirementItems(context) {
var notes = context.programNotes;
var items = notes.map(function (note) {
return formatProgramNote(note, "requirement");
}).filter(Boolean);
if (!items.length && context.programSummary) {
items.push("Project program to be developed around " + context.programSummary);
}
return items;
}
function formattedAssumptionItems(context) {
var notes = context.programNotes;
return notes.map(function (note) {
return formatProgramNote(note, "assumption");
}).filter(Boolean);
}
function formatProgramNote(note, mode) {
var text = normalizeProgramNote(note);
var lower = text.toLowerCase();
var number = text.match(/(?:approximately\s*)?[\d,.]+(?:\s*-\s*[\d,.]+)?/i);
var amount = number ? number[0].replace(/^approximately\s*/i, "") : "";
var prefix = mode === "assumption" ? "Assume " : "";
if (/no\s+retail/.test(lower)) {
return mode === "assumption" ? "Assume no retail component is included unless later directed by Owner" : "No retail component is included in the current program";
}
if (/unit/.test(lower) && /average|avg/.test(lower)) {
return prefix + "average residential unit size of " + (amount || text);
}
if (/unit/.test(lower) && amount) {
return prefix + "residential program of approximately " + amount + " units";
}
if (/one|two|three|bedroom|br\b/.test(lower)) {
return prefix + "unit mix to be planned around " + lowerFirst(text);
}
if (/parking|cars?|garage/.test(lower) && /ratio/.test(lower)) {
return prefix + "parking ratio to follow " + lowerFirst(text);
}
if (/parking|cars?|garage/.test(lower) && amount) {
return prefix + "parking capacity to accommodate approximately " + amount + " cars";
}
if (/parking|garage/.test(lower)) {
return prefix + "structured parking to be incorporated into the project planning";
}
if (/stor(?:y|ies)|floor|height/.test(lower) && amount) {
return prefix + "building height of approximately " + amount + " stories";
}
if (/amenit/.test(lower) && amount) {
return prefix + "amenity program of approximately " + amount + " square feet";
}
if (/amenit/.test(lower)) {
return prefix + "interior and exterior amenity spaces to be included in the program";
}
if (/podium/.test(lower)) {
return prefix + "podium configuration to be studied as part of the building concept";
}
if (/high-rise|high rise|tower/.test(lower)) {
return prefix + "high-rise building configuration to be evaluated through massing and planning studies";
}
if (/office/.test(lower) && /retail/.test(lower)) {
return prefix + "office and retail program areas to be coordinated within the planning concept";
}
if (/office/.test(lower)) {
return prefix + "office program requirements to be incorporated into planning, core, and service studies";
}
if (/retail|restaurant|storefront/.test(lower)) {
return prefix + "retail or storefront program requirements to be coordinated with ground-level planning";
}
if (/budget|cost/.test(lower)) {
return prefix + "design decisions to remain aligned with the Owner's budget and cost expectations";
}
if (/schedule|milestone/.test(lower)) {
return prefix + "design process to be coordinated with the Owner's schedule and milestone requirements";
}
if (/timeless|character|architectural|design/.test(lower)) {
return prefix + "architectural character to reflect " + lowerFirst(text);
}
if (!text || text.length < 4) {
return "";
}
return mode === "assumption" ? "Assume " + lowerFirst(text) : "Program requirement for " + lowerFirst(text);
}
function normalizeProgramNote(value) {
return String(value || "")
.replace(/^[\s\-*•]+/, "")
.replace(/\bSF\b/gi, "square feet")
.replace(/\bsq\.?\s*ft\.?\b/gi, "square feet")
.replace(/\s+/g, " ")
.trim();
}
function isParagraphHeadingLine(value) {
var text = String(value || "").trim();
if (!text || isBulletLine(text) || text.length > 72) {
return false;
}
if (/:$/.test(text)) {
return true;
}
if (/^\d+\.\s+[A-Z][^.]{0,60}$/.test(text)) {
return true;
}
return /^(?:scope of work|basic services|deliverables|m\+a assumptions|bidding and negotiation|construction administration|our design team|project description)$/i.test(text);
}
function isSalutationLine(value) {
return /^Dear\b.*?,\s*$/i.test(String(value || "").trim());
}
function bulletText(value) {
return cleanLines(String(value || "").split(/\r?\n/)).map(function (line) {
return "- " + line.replace(/^(?:[•*-]|•|•)\s+/, "");
}).join("\n");
}
function renderScheduleTable() {
var rows = [
["Conceptual Design", proposal.schedule.concept],
["Schematic Design", proposal.schedule.schematic],
["Design Development", proposal.schedule.designDevelopment],
["Construction Documents", proposal.schedule.constructionDocuments],
["Bidding", proposal.schedule.bidding],
["Permit and IFC", proposal.schedule.permitIfc],
["Construction Administration", proposal.schedule.constructionAdministration]
];
return '
Phase
Duration
' + rows.map(function (row) {
return "
" + escapeHtml(row[0]) + "
" + escapeHtml(row[1]) + "
";
}).join("") + "
MA and the AOR will work out a mutually agreeable schedule with the Owner if the above do not correspond with Owner’s expectations. Durations above do not include approval times by Owner.
";
}).join("");
}
function renderCompensationHtml() {
return '
The fees for the Basic Architectural Design Services described above will be a lump sum of $' + escapeHtml(proposal.compensation.amount) + ' (' + escapeHtml(proposal.compensation.amountWords) + ') billed monthly and calculated in the following manner:
";
}
function renderFeeScheduleHtml() {
if (!proposal.feeSchedule.name) {
return '
Drag and drop a fee schedule in the Project Info tab.
';
}
if (/^data:image\//.test(proposal.feeSchedule.dataUrl)) {
return '
' + escapeHtml(proposal.feeSchedule.name) + '
';
}
return '
' + escapeHtml(proposal.feeSchedule.name) + ' PDF fee schedule attached to draft. Use the exported data if the PDF itself needs to be carried forward.
';
}
function consultantGroups() {
var groups = ["Primary", "Secondary", "Owner coordinated"];
return groups.map(function (name) {
return {
name: name === "Primary" ? "Additional primary consultants that will be needed for the project’s design are:" : name === "Secondary" ? "Other additional secondary consultants that will be needed are:" : "The following consultants will be Owner coordinated:",
items: proposal.consultants.filter(function (item) {
return item.group === name && item.included;
})
};
}).filter(function (group) {
return group.items.length;
});
}
function textToHtml(value) {
var lines = String(value || "").split(/\r?\n/);
var html = [];
var list = [];
function flushList() {
if (list.length) {
html.push("
" + list.map(function (item) { return "
" + escapeProposalCopy(item) + "
"; }).join("") + "
");
list = [];
}
}
lines.forEach(function (line) {
var trimmed = line.trim();
if (!trimmed) {
flushList();
return;
}
if (/^[•*-]\s+/.test(trimmed)) {
list.push(trimmed.replace(/^[•*-]\s+/, ""));
return;
}
flushList();
html.push("
" + escapeProposalCopy(trimmed) + "
");
});
flushList();
return html.join("");
}
function textToHtmlContinuous(value, options) {
if (options && options.joinMidSentenceParagraphs) {
value = joinMidSentenceParagraphBreaks(value);
}
var lines = String(value || "").split(/\r?\n/);
var plainHeadingPattern = options && options.plainHeadingPattern;
var html = [];
var list = [];
var paragraph = [];
function flushParagraph() {
if (paragraph.length) {
var lineSeparator = options && options.joinSoftLineBreaks ? " " : " ";
html.push("
';
var remove = document.createElement("button");
remove.type = "button";
remove.className = "danger compact";
remove.textContent = "Remove";
remove.addEventListener("click", function () {
deletePhotoData(customCompensationAssetId());
proposal.customCompensation = { enabled: false, name: "", dataUrl: "", stored: false, orientation: "" };
markDirty();
saveCurrentDraft();
renderAll();
});
summary.appendChild(remove);
card.appendChild(summary);
}
return card;
}
async function importCustomCompensationPdf(file, input) {
if (!file || (!/\.pdf$/i.test(file.name || "") && file.type !== "application/pdf")) {
window.alert("Please choose a PDF file.");
input.value = "";
return;
}
flashStatus("Checking Compensation PDF...");
try {
if (window.location.protocol === "file:") {
throw new Error("PDF Compensation page uploads require the local Proposal Bot server. Open the app with start-proposal-bot.bat and try again.");
}
var pdfjs = await import("./assets/vendor/pdfjs/pdf.min.js");
pdfjs.GlobalWorkerOptions.workerSrc = new URL("assets/vendor/pdfjs/pdf.worker.min.js", window.location.href).href;
var bytes = new Uint8Array(await file.arrayBuffer());
var documentTask = pdfjs.getDocument({ data: bytes });
var pdf = await documentTask.promise;
if (pdf.numPages !== 1) {
throw new Error("The Compensation PDF must contain exactly one page.");
}
var page = await pdf.getPage(1);
var viewport = page.getViewport({ scale: 1 });
var shortSide = Math.min(viewport.width, viewport.height);
var longSide = Math.max(viewport.width, viewport.height);
if (Math.abs(shortSide - 612) > 4 || Math.abs(longSide - 792) > 4) {
throw new Error("The Compensation PDF must be Letter size (8.5 × 11 inches).");
}
var orientation = viewport.width > viewport.height ? "landscape" : "portrait";
var requiredOrientation = currentThemeId() === "modernLandscape" ? "landscape" : "portrait";
if (orientation !== requiredOrientation) {
throw new Error("The selected theme uses " + requiredOrientation + " pages. Please upload a " + requiredOrientation + " Letter PDF or change themes first.");
}
var printViewport = page.getViewport({ scale: 2.5 });
var canvas = document.createElement("canvas");
canvas.width = Math.ceil(printViewport.width);
canvas.height = Math.ceil(printViewport.height);
var context = canvas.getContext("2d", { alpha: false });
context.fillStyle = "#ffffff";
context.fillRect(0, 0, canvas.width, canvas.height);
await page.render({ canvasContext: context, viewport: printViewport }).promise;
var dataUrl = canvas.toDataURL("image/jpeg", 0.96);
proposal.customCompensation = {
enabled: true,
name: file.name || "Uploaded compensation.pdf",
dataUrl: dataUrl,
stored: false,
orientation: orientation
};
storeCustomCompensation(function (stored) {
proposal.customCompensation.stored = stored;
markDirty();
saveCurrentDraft();
renderAll();
flashStatus("Custom Compensation PDF added.");
});
if (typeof pdf.destroy === "function") {
pdf.destroy();
}
} catch (error) {
console.error(error);
window.alert(error && error.message ? error.message : "The Compensation PDF could not be read.");
flashStatus("Compensation PDF not added.");
} finally {
input.value = "";
}
}
function renderFeeStructureEditors() {
var wrap = document.createElement("div");
wrap.className = "fee-structure-editors";
proposal.feeStructures.forEach(function (structure, structureIndex) {
var section = document.createElement("section");
section.className = "panel-card fee-structure-editor";
var head = document.createElement("div");
head.className = "panel-head";
head.innerHTML = "
Fee Structure " + (structureIndex + 1) + "
The bold text appears above this chart; the supporting text appears below it.
";
if (proposal.feeStructures.length > 1) {
var remove = document.createElement("button");
remove.type = "button";
remove.className = "secondary compact";
remove.textContent = "Remove";
remove.addEventListener("click", function () {
proposal.feeStructures.splice(structureIndex, 1);
markDirty();
saveCurrentDraft();
renderAll();
});
head.appendChild(remove);
}
section.appendChild(head);
section.appendChild(renderFeeStructureTextField(structureIndex, "heading", "Bold text above chart", "Optional heading or introduction"));
section.appendChild(renderFeeStructurePhaseNamesEditor(structureIndex));
var grid = document.createElement("div");
grid.className = "field-grid";
[
["concept", feeStructurePhaseLabel(structure, "concept")],
["schematic", feeStructurePhaseLabel(structure, "schematic")],
["designDevelopment", feeStructurePhaseLabel(structure, "designDevelopment")],
["constructionDocuments", feeStructurePhaseLabel(structure, "constructionDocuments")],
["bid", feeStructurePhaseLabel(structure, "bid")],
["constructionObservation", feeStructurePhaseLabel(structure, "constructionObservation")]
].forEach(function (field) {
grid.appendChild(renderFeeStructureCurrencyField(structureIndex, field[0], field[1]));
});
section.appendChild(grid);
section.appendChild(renderFeeStructureTextField(structureIndex, "belowText", "Text below chart", "Optional supporting text"));
wrap.appendChild(section);
});
if (proposal.feeStructures.length < 3) {
var add = document.createElement("button");
add.type = "button";
add.className = "secondary compact";
add.textContent = "Add fee structure";
add.addEventListener("click", function () {
proposal.feeStructures.push(createFeeStructure());
markDirty();
saveCurrentDraft();
renderAll();
});
wrap.appendChild(add);
}
return wrap;
}
function renderFeeStructurePhaseNamesEditor(structureIndex) {
var structure = proposal.feeStructures[structureIndex];
var card = panelCard("Chart phase names", "These labels apply only to this fee structure's chart.");
var grid = document.createElement("div");
grid.className = "field-grid";
phaseLabelDefinitions().filter(function (phase) {
return phase.key !== "permitIfc";
}).forEach(function (phase) {
var field = document.createElement("div");
field.className = "field";
var label = document.createElement("label");
var input = document.createElement("input");
input.type = "text";
input.id = "fee-structure-" + structureIndex + "-phase-label-" + phase.key;
input.maxLength = 70;
input.value = feeStructurePhaseLabel(structure, phase.key);
label.setAttribute("for", input.id);
label.textContent = phase.label;
input.addEventListener("input", function () {
structure.phaseLabels[phase.key] = input.value;
markDirty();
renderPreview();
saveCurrentDraft();
});
input.addEventListener("change", function () {
structure.phaseLabels = normalizePhaseLabels(structure.phaseLabels);
markDirty();
saveCurrentDraft();
renderAll();
});
field.appendChild(label);
field.appendChild(input);
grid.appendChild(field);
});
card.appendChild(grid);
return card;
}
function renderFeeStructureTextField(structureIndex, key, labelText, placeholder) {
var field = document.createElement("div");
field.className = "field";
var label = document.createElement("label");
var input = document.createElement("textarea");
input.id = "fee-structure-" + structureIndex + "-" + key;
label.setAttribute("for", input.id);
label.textContent = labelText;
input.value = proposal.feeStructures[structureIndex][key] || "";
input.placeholder = placeholder;
input.addEventListener("input", function () {
proposal.feeStructures[structureIndex][key] = input.value;
markDirty();
renderPreview();
saveCurrentDraft();
});
field.appendChild(label);
field.appendChild(input);
return field;
}
function renderFeeStructureCurrencyField(structureIndex, key, labelText) {
var wrap = document.createElement("div");
wrap.className = "field";
var labelRow = document.createElement("div");
labelRow.className = "field-label-row";
var label = document.createElement("label");
var input = document.createElement("input");
input.type = "text";
input.id = "fee-structure-" + structureIndex + "-" + key;
label.setAttribute("for", input.id);
label.textContent = labelText;
labelRow.appendChild(label);
var toggleWrap = document.createElement("label");
toggleWrap.className = "field-chart-toggle";
var toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.checked = proposal.feeStructures[structureIndex].includedPhases[key] !== false;
toggle.addEventListener("change", function () {
proposal.feeStructures[structureIndex].includedPhases[key] = toggle.checked;
markDirty();
renderPreview();
saveCurrentDraft();
});
toggleWrap.appendChild(toggle);
toggleWrap.appendChild(document.createTextNode(" Show in chart"));
labelRow.appendChild(toggleWrap);
wrap.appendChild(labelRow);
input.value = formatCurrencyInput(proposal.feeStructures[structureIndex][key]);
input.addEventListener("input", function () {
proposal.feeStructures[structureIndex][key] = sanitizeCurrencyInput(input.value);
markDirty();
renderPreview();
saveCurrentDraft();
});
input.addEventListener("blur", function () {
input.value = formatCurrencyInput(proposal.feeStructures[structureIndex][key]);
});
wrap.appendChild(input);
return wrap;
}
function renderCompensationFeeNotesEditor() {
var wrap = document.createElement("div");
wrap.className = "panel-card";
var head = document.createElement("div");
head.className = "panel-head";
head.innerHTML = "
Fee Notes — edit the Compensation notes here
Default notes automatically follow the CA Design Schedule duration and compensated phases. Editing a note makes it custom; use Reset notes to default to restore automatic updates.
";
wrap.appendChild(head);
var toggleRow = document.createElement("label");
toggleRow.className = "inline-check";
var toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.checked = proposal.compensation.showFeeNotes !== false;
toggle.addEventListener("change", function () {
proposal.compensation.showFeeNotes = toggle.checked;
markDirty();
renderPreview();
saveCurrentDraft();
});
toggleRow.appendChild(toggle);
toggleRow.appendChild(document.createTextNode(" Show Fee Notes"));
wrap.appendChild(toggleRow);
var textarea = document.createElement("textarea");
textarea.id = "compensation-fee-notes";
textarea.value = compensationFeeNotesText();
textarea.placeholder = "One note per line";
textarea.addEventListener("input", function () {
proposal.compensation.feeNotesText = textarea.value;
proposal.compensation.feeNotesAuto = false;
markDirty();
renderPreview();
saveCurrentDraft();
});
wrap.appendChild(textarea);
var referenceField = document.createElement("div");
referenceField.className = "field full";
var referenceLabel = document.createElement("label");
referenceLabel.setAttribute("for", "compensation-fee-notes-reference");
referenceLabel.textContent = "Reference sentence";
referenceField.appendChild(referenceLabel);
var referenceInput = document.createElement("input");
referenceInput.id = "compensation-fee-notes-reference";
referenceInput.type = "text";
referenceInput.value = proposal.compensation.feeNotesReference;
referenceInput.addEventListener("input", function () {
proposal.compensation.feeNotesReference = referenceInput.value;
markDirty();
renderPreview();
saveCurrentDraft();
});
referenceField.appendChild(referenceInput);
wrap.appendChild(referenceField);
var actions = document.createElement("div");
actions.className = "section-editor-actions";
var reset = document.createElement("button");
reset.type = "button";
reset.className = "secondary compact";
reset.textContent = "Reset notes to default";
reset.addEventListener("click", function () {
proposal.compensation.feeNotesText = DEFAULT_COMPENSATION_FEE_NOTES_TEXT;
proposal.compensation.feeNotesAuto = true;
proposal.compensation.feeNotesReference = DEFAULT_COMPENSATION_FEE_NOTES_REFERENCE;
markDirty();
saveCurrentDraft();
renderAll();
});
actions.appendChild(reset);
wrap.appendChild(actions);
return wrap;
}
function renderCompensationListEditor(title, hint, textPath, togglePath, toggleLabel) {
var wrap = document.createElement("div");
wrap.className = "panel-card";
var head = document.createElement("div");
head.className = "panel-head";
head.innerHTML = "
" + escapeHtml(title) + "
" + escapeHtml(hint) + "
";
wrap.appendChild(head);
var toggleRow = document.createElement("label");
toggleRow.className = "inline-check";
var checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = getValue(proposal, togglePath) === true;
checkbox.addEventListener("change", function () {
setValue(proposal, togglePath, checkbox.checked);
markDirty();
renderPreview();
saveCurrentDraft();
});
toggleRow.appendChild(checkbox);
toggleRow.appendChild(document.createTextNode(toggleLabel));
wrap.appendChild(toggleRow);
var textarea = document.createElement("textarea");
textarea.value = getValue(proposal, textPath) || "";
textarea.placeholder = "One item per line";
textarea.addEventListener("input", function () {
setValue(proposal, textPath, textarea.value);
markDirty();
renderPreview();
saveCurrentDraft();
});
wrap.appendChild(textarea);
return wrap;
}
function renderCurrencyField(path, labelText, emphasize, feeKey) {
var wrap = document.createElement("div");
wrap.className = "field";
var label = document.createElement("label");
if (feeKey) {
var labelRow = document.createElement("div");
labelRow.className = "field-label-row";
label.textContent = labelText;
labelRow.appendChild(label);
var toggleWrap = document.createElement("label");
toggleWrap.className = "field-chart-toggle";
var toggle = document.createElement("input");
toggle.type = "checkbox";
toggle.checked = proposal.feeSchedule.includedPhases[feeKey] !== false;
toggle.addEventListener("change", function () {
proposal.feeSchedule.includedPhases[feeKey] = toggle.checked;
markDirty();
saveCurrentDraft();
renderPreview();
});
toggleWrap.appendChild(toggle);
toggleWrap.appendChild(document.createTextNode(" Show in chart"));
labelRow.appendChild(toggleWrap);
wrap.appendChild(labelRow);
} else {
label.textContent = labelText;
wrap.appendChild(label);
}
var input = document.createElement("input");
input.type = "text";
input.id = "field-" + path.replace(/\./g, "-");
label.setAttribute("for", input.id);
input.value = formatCurrencyInput(getValue(proposal, path) || "");
if (emphasize) {
input.className = "currency-emphasis currency-total-input";
}
input.addEventListener("input", function () {
setValue(proposal, path, sanitizeCurrencyInput(input.value));
if (path !== "compensation.amount") {
proposal.compensation.amount = calculateCompensationTotal();
var totalInput = document.querySelector(".currency-total-input");
if (totalInput) {
totalInput.value = formatCurrencyInput(proposal.compensation.amount);
}
}
proposal.compensation.amountWords = compensationAmountToWords(proposal.compensation.amount);
var wordsInput = document.getElementById("field-compensation-amountWords");
if (wordsInput) {
wordsInput.value = proposal.compensation.amountWords;
}
markDirty();
renderPreview();
});
input.addEventListener("blur", function () {
input.value = formatCurrencyInput(getValue(proposal, path) || "");
});
wrap.appendChild(input);
return wrap;
}
function renderConsultantsCard() {
var card = panelCard("Consultants", "Choose consultants for this proposal, or add a new consultant to the appropriate list.");
var groupLabels = {
"Needed": "Primary Consultants",
"May be needed": "Secondary Consultants (as needed)",
"Client coordinated": "Client Coordinated Consultants"
};
["Needed", "May be needed", "Client coordinated"].forEach(function (groupName) {
var groupWrap = document.createElement("div");
groupWrap.className = "consultant-group-card";
groupWrap.innerHTML = "" + escapeHtml(groupLabels[groupName]) + "";
var list = document.createElement("div");
list.className = "option-list";
var groupConsultants = proposal.consultants.filter(function (consultant) {
return consultant.group === groupName;
});
groupConsultants.forEach(function (consultant, orderIndex) {
var itemWrap = document.createElement("div");
itemWrap.className = "consultant-item-row";
var row = document.createElement("button");
row.type = "button";
row.className = "option-row" + (consultant.included ? " active" : "");
row.innerHTML = "" + escapeHtml(consultant.name) + "" + (consultant.included ? "Included in this proposal" : "Not included in this proposal") + "";
row.addEventListener("click", function () {
consultant.included = !consultant.included;
markDirty();
renderAll();
});
itemWrap.appendChild(row);
var orderControls = document.createElement("div");
orderControls.className = "consultant-order-controls";
[
{ label: "Up", offset: -1 },
{ label: "Down", offset: 1 }
].forEach(function (control) {
var button = document.createElement("button");
button.type = "button";
button.className = "secondary compact";
button.textContent = control.label;
button.disabled = orderIndex + control.offset < 0 || orderIndex + control.offset >= groupConsultants.length;
button.setAttribute("aria-label", "Move " + consultant.name + " " + control.label.toLowerCase());
button.addEventListener("click", function () {
var other = groupConsultants[orderIndex + control.offset];
var currentIndex = proposal.consultants.indexOf(consultant);
var otherIndex = proposal.consultants.indexOf(other);
proposal.consultants[currentIndex] = other;
proposal.consultants[otherIndex] = consultant;
markDirty();
saveCurrentDraft();
renderAll();
});
orderControls.appendChild(button);
});
itemWrap.appendChild(orderControls);
list.appendChild(itemWrap);
});
groupWrap.appendChild(list);
var addRow = document.createElement("div");
addRow.className = "consultant-add-row";
var input = document.createElement("input");
input.type = "text";
input.placeholder = "Add consultant";
input.setAttribute("aria-label", "Add a consultant to " + groupLabels[groupName]);
var addButton = document.createElement("button");
addButton.type = "button";
addButton.className = "secondary compact";
addButton.textContent = "Add";
var addConsultant = function () {
var name = input.value.trim();
if (!name) {
input.focus();
return;
}
proposal.consultants.push({ name: name, group: groupName, included: true });
markDirty();
saveCurrentDraft();
renderAll();
};
addButton.addEventListener("click", addConsultant);
input.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
event.preventDefault();
addConsultant();
}
});
addRow.appendChild(input);
addRow.appendChild(addButton);
groupWrap.appendChild(addRow);
card.appendChild(groupWrap);
});
return card;
}
function renderQualificationsCard() {
var card = panelCard("Our Qualifications", "Edit the narrative and highlighted strengths shown on the qualifications page.");
var overview = renderField(fieldDef("qualifications.overview", "Overview", "textarea", true));
card.appendChild(overview);
var stats = document.createElement("div");
stats.className = "qualifications-grid";
proposal.qualifications.stats.forEach(function (stat, index) {
var statCard = document.createElement("div");
statCard.className = "qualification-stat-editor";
statCard.appendChild(renderNestedField("qualifications.stats." + index + ".value", "Value"));
statCard.appendChild(renderNestedField("qualifications.stats." + index + ".label", "Label"));
statCard.appendChild(renderNestedField("qualifications.stats." + index + ".detail", "Detail", "textarea"));
stats.appendChild(statCard);
});
card.appendChild(stats);
return card;
}
function renderTeamCard() {
var card = panelCard("Our Team", "Choose how many people are highlighted and edit each person's portrait, name, role, and supporting note.");
var countField = document.createElement("div");
countField.className = "field";
countField.innerHTML = "";
var countSelect = document.createElement("select");
[1, 2, 3, 4].forEach(function (count) {
var option = document.createElement("option");
option.value = String(count);
option.textContent = String(count);
countSelect.appendChild(option);
});
countSelect.value = String(proposal.team.highlightCount);
countSelect.addEventListener("change", function () {
proposal.team.highlightCount = clampTeamCount(countSelect.value);
markDirty();
renderPreview();
});
countField.appendChild(countSelect);
card.appendChild(countField);
var shapeField = document.createElement("div");
shapeField.className = "field";
shapeField.innerHTML = "";
var shapeSelect = document.createElement("select");
[
{ value: "square", label: "Square" },
{ value: "circle", label: "Circle" }
].forEach(function (optionDef) {
var option = document.createElement("option");
option.value = optionDef.value;
option.textContent = optionDef.label;
shapeSelect.appendChild(option);
});
shapeSelect.value = normalizeHeadshotShape(proposal.team.headshotShape);
shapeSelect.addEventListener("change", function () {
proposal.team.headshotShape = normalizeHeadshotShape(shapeSelect.value);
markDirty();
renderPreview();
saveCurrentDraft();
});
shapeField.appendChild(shapeSelect);
card.appendChild(shapeField);
proposal.team.members.forEach(function (member, index) {
var memberCard = document.createElement("div");
memberCard.className = "team-member-editor";
memberCard.appendChild(renderNestedField("team.members." + index + ".name", "Name"));
memberCard.appendChild(renderNestedField("team.members." + index + ".title", "Role"));
memberCard.appendChild(renderNestedField("team.members." + index + ".description", "Description", "textarea"));
var uploadField = document.createElement("div");
uploadField.className = "field full";
uploadField.innerHTML = "";
var upload = document.createElement("input");
upload.type = "file";
upload.accept = "image/*";
upload.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (!file) {
return;
}
readFileAsDataUrl(file, function (dataUrl) {
proposal.team.members[index].photoDataUrl = dataUrl;
storeTeamMemberPhoto(proposal.team.members[index], function (stored) {
proposal.team.members[index].photoStored = stored;
markDirty();
saveCurrentDraft();
renderAll();
});
});
});
uploadField.appendChild(upload);
if (member.photoDataUrl) {
uploadField.insertAdjacentHTML("beforeend", '');
var clearPhoto = document.createElement("button");
clearPhoto.type = "button";
clearPhoto.className = "secondary compact";
clearPhoto.textContent = "Clear photo";
clearPhoto.addEventListener("click", function () {
deletePhotoData(teamMemberPhotoAssetId(proposal.team.members[index]));
proposal.team.members[index].photoDataUrl = "";
proposal.team.members[index].photoStored = false;
markDirty();
saveCurrentDraft();
renderAll();
});
uploadField.appendChild(clearPhoto);
}
memberCard.appendChild(uploadField);
card.appendChild(memberCard);
});
return card;
}
function renderSignatureCard() {
var card = panelCard("Signature", "Upload a signature that appears above the typed name on the Thank You page.");
card.appendChild(renderField(fieldDef("signature.name", "Typed name")));
card.appendChild(renderField(fieldDef("signature.title", "Title")));
card.appendChild(renderField(fieldDef("signature.company", "Company", "textarea", true)));
card.appendChild(renderField(fieldDef("signature.acceptedDate", "Accepted date")));
var uploadField = document.createElement("div");
uploadField.className = "field full";
uploadField.innerHTML = "";
var upload = document.createElement("input");
upload.type = "file";
upload.accept = "image/*";
upload.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (!file) {
return;
}
readFileAsDataUrl(file, function (dataUrl) {
proposal.signature.imageDataUrl = dataUrl;
storeSignatureImage(function (stored) {
proposal.signature.imageStored = stored;
markDirty();
saveCurrentDraft();
renderAll();
});
});
});
uploadField.appendChild(upload);
if (proposal.signature.imageDataUrl) {
uploadField.insertAdjacentHTML("beforeend", '');
}
card.appendChild(uploadField);
return card;
}
function renderGeneralConditionsEditorCard() {
proposal.generalConditions = normalizeGeneralConditions(proposal.generalConditions);
var card = panelCard("General Conditions Text", "Edit the conditions that appear under Attachment A. Keep “hourly billing rates:” on its own line so the chart stays in the intended location.");
var field = document.createElement("div");
field.className = "field full";
var label = document.createElement("label");
label.setAttribute("for", "general-conditions-text");
label.textContent = "Default General Conditions text";
field.appendChild(label);
var textarea = document.createElement("textarea");
textarea.id = "general-conditions-text";
textarea.value = proposal.generalConditions.text;
textarea.addEventListener("input", function () {
proposal.generalConditions.text = textarea.value;
markDirty();
renderPreview();
saveCurrentDraft();
});
field.appendChild(textarea);
card.appendChild(field);
var actions = document.createElement("div");
actions.className = "section-editor-actions";
var resetText = document.createElement("button");
resetText.type = "button";
resetText.className = "secondary compact";
resetText.textContent = "Reset text to default";
resetText.addEventListener("click", function () {
proposal.generalConditions.text = DEFAULT_GENERAL_CONDITIONS_TEXT;
markDirty();
saveCurrentDraft();
renderAll();
});
actions.appendChild(resetText);
card.appendChild(actions);
card.appendChild(renderGeneralConditionsRateEditorCard());
return card;
}
function renderGeneralConditionsRateEditorCard() {
var card = panelCard("Hourly Billing Rates", "Edit the role names and hourly amounts shown in the rate chart.");
var saveForFuture = document.createElement("button");
saveForFuture.type = "button";
saveForFuture.className = "secondary compact";
saveForFuture.textContent = "Save rates for future use";
saveForFuture.addEventListener("click", function () {
if (!writeSavedGeneralConditionsHourlyRates(proposal.generalConditions.hourlyRates)) {
flashStatus("Hourly rates could not be saved.");
return;
}
renderAll();
flashStatus("Hourly rates saved for future proposals.");
});
card.querySelector(".panel-head").appendChild(saveForFuture);
var list = document.createElement("div");
list.className = "general-conditions-rate-editor-list";
proposal.generalConditions.hourlyRates.forEach(function (rate, index) {
var row = document.createElement("div");
row.className = "general-conditions-rate-editor-row";
var roleField = document.createElement("div");
roleField.className = "field";
var roleLabel = document.createElement("label");
roleLabel.setAttribute("for", "general-conditions-rate-label-" + index);
roleLabel.textContent = "Role";
roleField.appendChild(roleLabel);
var roleInput = document.createElement("input");
roleInput.id = "general-conditions-rate-label-" + index;
roleInput.type = "text";
roleInput.value = rate.label;
roleInput.addEventListener("input", function () {
updateGeneralConditionsRate(index, "label", roleInput.value);
markDirty();
renderPreview();
saveCurrentDraft();
});
roleField.appendChild(roleInput);
row.appendChild(roleField);
var amountField = document.createElement("div");
amountField.className = "field";
var amountLabel = document.createElement("label");
amountLabel.setAttribute("for", "general-conditions-rate-amount-" + index);
amountLabel.textContent = "Hourly rate";
amountField.appendChild(amountLabel);
var amountInput = document.createElement("input");
amountInput.id = "general-conditions-rate-amount-" + index;
amountInput.type = "text";
amountInput.inputMode = "decimal";
amountInput.value = formatHourlyRateInput(rate.amount);
amountInput.addEventListener("input", function () {
updateGeneralConditionsRate(index, "amount", amountInput.value);
markDirty();
renderPreview();
saveCurrentDraft();
});
amountInput.addEventListener("blur", function () {
amountInput.value = formatHourlyRateInput(generalConditionsRateAt(index).amount);
});
amountField.appendChild(amountInput);
row.appendChild(amountField);
if (proposal.generalConditions.hourlyRates.length > 1) {
var remove = document.createElement("button");
remove.type = "button";
remove.className = "danger compact general-conditions-rate-remove";
remove.textContent = "Remove";
remove.addEventListener("click", function () {
proposal.generalConditions.hourlyRates.splice(index, 1);
markDirty();
saveCurrentDraft();
renderAll();
});
row.appendChild(remove);
}
list.appendChild(row);
});
card.appendChild(list);
var actions = document.createElement("div");
actions.className = "section-editor-actions";
var add = document.createElement("button");
add.type = "button";
add.className = "secondary compact";
add.textContent = "Add rate";
add.addEventListener("click", function () {
proposal.generalConditions.hourlyRates.push({ label: "", amount: "" });
markDirty();
saveCurrentDraft();
renderAll();
});
actions.appendChild(add);
var savedRates = readSavedGeneralConditionsHourlyRates();
if (savedRates.length) {
var useSaved = document.createElement("button");
useSaved.type = "button";
useSaved.className = "secondary compact";
useSaved.textContent = "Use saved rates";
useSaved.addEventListener("click", function () {
proposal.generalConditions.hourlyRates = copyGeneralConditionsHourlyRates(savedRates);
markDirty();
saveCurrentDraft();
renderAll();
flashStatus("Saved hourly rates applied.");
});
actions.appendChild(useSaved);
}
var reset = document.createElement("button");
reset.type = "button";
reset.className = "secondary compact";
reset.textContent = "Reset rates to default";
reset.addEventListener("click", function () {
proposal.generalConditions.hourlyRates = defaultGeneralConditionsHourlyRates();
markDirty();
saveCurrentDraft();
renderAll();
});
actions.appendChild(reset);
card.appendChild(actions);
return card;
}
function generalConditionsRateAt(index) {
proposal.generalConditions = normalizeGeneralConditions(proposal.generalConditions);
if (!proposal.generalConditions.hourlyRates[index]) {
proposal.generalConditions.hourlyRates[index] = { label: "", amount: "" };
}
return proposal.generalConditions.hourlyRates[index];
}
function updateGeneralConditionsRate(index, field, value) {
var rate = generalConditionsRateAt(index);
rate[field] = field === "amount" ? sanitizeCurrencyInput(value) : String(value || "");
}
function renderClientLogosCard() {
var card = panelCard("Client Logos", "Upload up to two client logos for the Northline cover footer.");
var logos = proposal.project && Array.isArray(proposal.project.clientLogos) ? proposal.project.clientLogos : [];
logos.forEach(function (logo, index) {
var uploadField = document.createElement("div");
uploadField.className = "field full";
uploadField.innerHTML = "";
var upload = document.createElement("input");
upload.type = "file";
upload.accept = "image/*";
upload.addEventListener("change", function (event) {
var file = event.target.files && event.target.files[0];
if (!file) {
return;
}
readFileAsDataUrl(file, function (dataUrl) {
proposal.project.clientLogos[index] = {
name: file.name,
dataUrl: dataUrl,
stored: false
};
storeClientLogo(index, function (stored) {
proposal.project.clientLogos[index].stored = stored;
markDirty();
saveCurrentDraft();
renderAll();
});
});
});
uploadField.appendChild(upload);
if (logo && logo.dataUrl) {
uploadField.insertAdjacentHTML("beforeend", '');
var clearLogo = document.createElement("button");
clearLogo.type = "button";
clearLogo.className = "secondary compact";
clearLogo.textContent = "Remove logo";
clearLogo.addEventListener("click", function () {
proposal.project.clientLogos[index] = { name: "", dataUrl: "", stored: false };
markDirty();
saveCurrentDraft();
renderAll();
});
uploadField.appendChild(clearLogo);
}
card.appendChild(uploadField);
});
els.editor.appendChild(card);
}
function renderNestedField(path, labelText, type) {
return renderField(fieldDef(path, labelText, type || "text", type === "textarea"));
}
function renderSectionsPanel() {
els.editor.innerHTML = "";
var section = sectionDefinitions.find(function (item) { return item.id === activeSectionId; }) || sectionDefinitions[0];
var phaseId = deliverablePhaseIdForSection(section.id);
if (phaseId && phaseId !== "conceptDesign") {
ensurePhaseScopeItems(phaseId, section);
}
var card = document.createElement("section");
card.className = "section-editor";
card.innerHTML = '
';
}
function defaultSummaryProposalTitle() {
var c = projectContext();
return "Proposal for " + sentenceCase(c.scopeSummary) + " for " + c.projectName + ".";
}
function projectSummaryProposalTitleHtml() {
var text = String((proposal.project && proposal.project.summaryProposalTitle) || "").trim() || defaultSummaryProposalTitle();
return '
' +
'' +
'
' + escapeHtml(text) + '
' +
'' +
'
';
}
function projectSummaryBlocksForPaging() {
var c = projectContext();
var summarySection = sectionDefinitions.find(function (item) { return item.id === "projectSummary"; });
var customSummaryText = summarySection && Object.prototype.hasOwnProperty.call(proposal.sectionOverrides, "projectSummary")
? proposal.sectionOverrides.projectSummary
: "";
var introText = normalizeProjectSummaryNarrativeText(customSummaryText).trim() || defaultProjectSummaryNarrative();
var blocks = mergeProjectSummarySentenceContinuations(textBlocksForPaging(introText));
if (proposal.project.showOwnerRequirements !== false) {
blocks.push("Basic owner requirements:");
blocks = blocks.concat(textBlocksForPaging(bulletText((c.ownerRequirements.length ? c.ownerRequirements : inferredOwnerRequirements()).join("\n"))));
}
if (proposal.project.showOwnerProvidedInfo !== false) {
blocks.push("The following information will support alignment as the work develops:");
blocks = blocks.concat(textBlocksForPaging(bulletText((c.ownerProvidedInfo.length ? c.ownerProvidedInfo : defaultOwnerProvidedItems()).join("\n"))));
}
if (proposal.project.showSummaryDeliverables === true && c.summaryDeliverables.length) {
blocks.push("Deliverables:");
blocks = blocks.concat(textBlocksForPaging(bulletText(c.summaryDeliverables.join("\n"))));
}
return blocks;
}
function mergeProjectSummarySentenceContinuations(blocks) {
return (blocks || []).reduce(function (merged, block) {
var current = String(block || "").trim();
var previous = merged.length ? merged[merged.length - 1] : "";
if (
current &&
previous &&
!isBulletLine(previous) &&
!isBulletLine(current) &&
!isParagraphHeadingLine(previous) &&
!isParagraphHeadingLine(current) &&
!/[.!?…:;\"')\]]$/.test(previous)
) {
merged[merged.length - 1] = previous + "\n" + current;
} else if (current) {
merged.push(current);
}
return merged;
}, []);
}
function renderProjectSummaryPageHtml(chunk, includeAddressBlock) {
var summaryText = chunk || joinPagingBlocks(projectSummaryBlocksForPaging());
return (includeAddressBlock ? projectSummaryAddressBlockHtml() + projectSummaryProposalTitleHtml() : "") + renderProjectSummaryNarrativeHtml(summaryText, includeAddressBlock);
}
function renderProjectSummaryNarrativeHtml(text, includeFixedHeading) {
var blocks = mergeProjectSummarySentenceContinuations(textBlocksForPaging(text));
var normalizedText = joinPagingBlocks(blocks);
if (!includeFixedHeading || !blocks.length) {
return textToHtmlContinuous(keepProjectSummaryNamesTogether(normalizedText), {
joinMidSentenceParagraphs: true,
joinSoftLineBreaks: true
});
}
var introBlockCount = 1;
if (proposal.projectSummarySections && typeof proposal.projectSummarySections === "object" && String(proposal.projectSummarySections.intro || "").trim()) {
introBlockCount = mergeProjectSummarySentenceContinuations(textBlocksForPaging(proposal.projectSummarySections.intro)).length || 1;
}
introBlockCount = Math.min(introBlockCount, blocks.length);
var intro = blocks.slice(0, introBlockCount).join("\n\n");
var remaining = blocks.slice(introBlockCount).join("\n\n");
var narrativeRenderOptions = {
joinMidSentenceParagraphs: true,
joinSoftLineBreaks: true
};
return textToHtmlContinuous(keepProjectSummaryNamesTogether(intro), narrativeRenderOptions) +
'
' + escapeHtml(PROJECT_SUMMARY_FIXED_HEADING) + '
' +
textToHtmlContinuous(keepProjectSummaryNamesTogether(remaining), narrativeRenderOptions);
}
function renderQualificationsPageHtml() {
var sectionText = renderPageSectionText("ourQualifications");
var stats = proposal.qualifications.stats.filter(function (item) {
return item && (item.value || item.label || item.detail);
});
return '
';
}
function renderGeneralConditionsPageHtml(chunk) {
var text = String(chunk === undefined ? renderPageSectionText("generalConditionsPage") : chunk);
var markerPattern = /hourly billing rates:/i;
var markerMatch = markerPattern.exec(text);
if (!markerMatch) {
return textToHtmlContinuous(text, {
plainHeadingPattern: /^The Owner will provide the following owner services:?$/i
});
}
var markerIndex = markerMatch.index;
var markerEnd = markerIndex + markerMatch[0].length;
var before = text.slice(0, markerEnd);
var after = text.slice(markerEnd);
var renderOptions = {
plainHeadingPattern: /^The Owner will provide the following owner services:?$/i
};
return textToHtmlContinuous(before, renderOptions) + renderGeneralConditionsRateChartHtml() + textToHtmlContinuous(after, renderOptions);
}
function renderGeneralConditionsRateChartHtml() {
var generalConditions = normalizeGeneralConditions(proposal.generalConditions);
return '
' +
'';
}
function compensationFeeNotesText() {
return proposal.compensation.feeNotesAuto === true
? buildAutomaticCompensationFeeNotes()
: String(proposal.compensation.feeNotesText || "");
}
function buildAutomaticCompensationFeeNotes() {
var compensatedPhases = compensatedFeePhases();
var designPhases = compensatedPhases.filter(function (phase) {
return phase.key !== "constructionObservation";
});
var notes = [
"Please note that no fees for consultants are included in this proposal. M+A Fees only.",
"Fees are valid for 90 days.",
"Fees will be billed monthly according to advance of work."
];
var caPhase = compensatedPhases.find(function (phase) {
return phase.key === "constructionObservation";
});
if (caPhase) {
notes.push("Fees include CA site visits twice monthly " + constructionAdministrationDuration() + ". In case construction lasts longer or more visits are required, additional fees will need to be negotiated.");
}
if (compensatedPhases.length) {
notes.push("The above fees apply when M+A is commissioned for the design architect role for " + humanList(compensatedPhases.map(function (phase) { return phase.label; })) + " scopes.");
}
if (designPhases.length) {
var disengagementPhase = designPhases.find(function (phase) {
return phase.key === "schematic";
}) || designPhases[designPhases.length - 1];
var completedPhases = designPhases.slice(0, designPhases.indexOf(disengagementPhase) + 1);
var feeReference = humanList(completedPhases.map(function (phase) { return phase.label; }));
notes.push("If, after " + disengagementPhase.label + ", the project is continued by another architect, a disengagement fee will be due. This fee will be 20% of the " + feeReference + (completedPhases.length === 1 ? " fee." : " fees."));
}
notes.push("If the program increases by more than 5%, fees may require adjustments.");
notes.push("Depending on the magnitude, additional changes during the compensated design services may require additional fees.");
return notes.join("\n");
}
function compensatedFeePhases() {
var phaseDefinitions = [
{ key: "concept", label: phaseLabel("concept") },
{ key: "schematic", label: phaseLabel("schematic") },
{ key: "designDevelopment", label: phaseLabel("designDevelopment") },
{ key: "constructionDocuments", label: phaseLabel("constructionDocuments") },
{ key: "bid", label: phaseLabel("bid") },
{ key: "constructionObservation", label: phaseLabel("constructionObservation") }
];
return phaseDefinitions.filter(function (phase) {
return proposal.feeStructures.some(function (structure) {
return structure.includedPhases[phase.key] !== false && Number(parseCompensationNumber(structure[phase.key]) || 0) > 0;
});
});
}
function constructionAdministrationDuration() {
var scheduleValue = String(proposal.schedule.constructionAdministration || "").trim();
var monthMatch = scheduleValue.match(/\b\d+(?:\.\d+)?\s*(?:months?|mos?\.?)\b/i);
return monthMatch ? "for " + monthMatch[0] : "during the scheduled " + phaseLabel("constructionObservation") + " period";
}
function humanList(items) {
var values = (items || []).filter(Boolean);
if (values.length < 2) {
return values[0] || "the compensated services";
}
if (values.length === 2) {
return values[0] + " and " + values[1];
}
return values.slice(0, -1).join(", ") + ", and " + values[values.length - 1];
}
function renderCompensationExtraListsHtml() {
var html = [];
if (proposal.compensation.showAdditionalServices === true) {
html.push(renderNumberedSectionHtml("Additional Services", proposal.compensation.additionalServicesText));
}
if (proposal.compensation.showOwnerProvidedServices === true) {
html.push(renderNumberedSectionHtml("Owner Provided Services", proposal.compensation.ownerProvidedServicesText));
}
return html.join("");
}
function joinMidSentenceParagraphBreaks(value) {
var lines = String(value || "").split(/\r?\n/);
var normalized = [];
for (var index = 0; index < lines.length; index += 1) {
var current = lines[index];
if (current.trim()) {
normalized.push(current);
continue;
}
var previous = normalized.length ? String(normalized[normalized.length - 1] || "").trim() : "";
var nextIndex = index + 1;
while (nextIndex < lines.length && !String(lines[nextIndex] || "").trim()) {
nextIndex += 1;
}
var next = nextIndex < lines.length ? String(lines[nextIndex] || "").trim() : "";
var shouldJoin = previous &&
next &&
!isBulletLine(previous) &&
!isBulletLine(next) &&
!isParagraphHeadingLine(previous) &&
!isParagraphHeadingLine(next) &&
!/[.!?…:;\"')\]]$/.test(previous);
if (shouldJoin) {
index = nextIndex - 1;
continue;
}
normalized.push("");
}
return normalized.join("\n");
}
function renderNumberedSectionHtml(title, text) {
var items = cleanLines(String(text || "").split(/\r?\n/));
if (!items.length) {
return "";
}
var intro = "";
var listItems = items;
if (title === "Additional Services" && items.length > 1) {
intro = "