Merge pull request #1095 from dod-ccpo/clin-errors

PoP Date Range
This commit is contained in:
leigh-mil 2019-09-30 11:12:54 -04:00 committed by GitHub
commit b7677018c2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 1015 additions and 483 deletions

View File

@ -2,6 +2,7 @@ import os
import re import re
import pathlib import pathlib
from configparser import ConfigParser from configparser import ConfigParser
from datetime import datetime
from flask import Flask, request, g, session from flask import Flask, request, g, session
from flask_session import Session from flask_session import Session
import redis import redis
@ -175,6 +176,12 @@ def map_config(config):
# with a Beat job once a day) # with a Beat job once a day)
"CELERY_RESULT_EXPIRES": 0, "CELERY_RESULT_EXPIRES": 0,
"CELERY_RESULT_EXTENDED": True, "CELERY_RESULT_EXTENDED": True,
"CONTRACT_START_DATE": datetime.strptime(
config.get("default", "CONTRACT_START_DATE"), "%Y-%m-%d"
).date(),
"CONTRACT_END_DATE": datetime.strptime(
config.get("default", "CONTRACT_END_DATE"), "%Y-%m-%d"
).date(),
} }

View File

@ -9,7 +9,6 @@ from wtforms.fields import (
from wtforms.fields.html5 import DateField from wtforms.fields.html5 import DateField
from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError from wtforms.validators import Required, Optional, Length, NumberRange, ValidationError
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from datetime import datetime
from numbers import Number from numbers import Number
from .data import JEDI_CLIN_TYPES from .data import JEDI_CLIN_TYPES
@ -85,12 +84,8 @@ class CLINForm(FlaskForm):
def validate(self, *args, **kwargs): def validate(self, *args, **kwargs):
valid = super().validate(*args, **kwargs) valid = super().validate(*args, **kwargs)
contract_start = datetime.strptime( contract_start = app.config.get("CONTRACT_START_DATE")
app.config.get("CONTRACT_START_DATE"), "%Y-%m-%d" contract_end = app.config.get("CONTRACT_END_DATE")
).date()
contract_end = datetime.strptime(
app.config.get("CONTRACT_END_DATE"), "%Y-%m-%d"
).date()
if ( if (
self.start_date.data self.start_date.data

View File

@ -163,6 +163,20 @@ describe('DateSelector', () => {
component.year = new Date().getFullYear() component.year = new Date().getFullYear()
expect(component.isYearValid).toEqual(true) expect(component.isYearValid).toEqual(true)
}) })
it('returns true when year is between min and max years', () => {
component.year = new Date('2019-01-01').getFullYear()
component.mindate = new Date('2018-01-01')
component.maxdate = new Date('2019-12-31')
expect(component.isYearValid).toEqual(true)
})
it('returns false when year is outside of min and max years', () => {
component.year = new Date('2020-01-01').getFullYear()
component.mindate = new Date('2018-01-01')
component.maxdate = new Date('2019-01-01')
expect(component.isYearValid).toEqual(false)
})
}) })
describe('formattedDate', () => { describe('formattedDate', () => {
@ -184,4 +198,56 @@ describe('DateSelector', () => {
expect(component.formattedDate).toEqual('01/22/1988') expect(component.formattedDate).toEqual('01/22/1988')
}) })
}) })
describe('isDateComplete', () => {
it('returns true if all fields are completed', () => {
component.day = 22
component.month = 1
component.year = 1988
expect(component.isDateComplete).toEqual(true)
})
it('returns false if all fields are not completed', () => {
component.day = 22
component.month = 1
component.year = 19
expect(component.isDateComplete).toEqual(false)
})
})
describe('minError', () => {
it('returns true if the date is before mindate', () => {
component.mindate = new Date('2020-01-01')
component.day = 1
component.month = 1
component.year = 2000
expect(component.minError).toEqual(true)
})
it('returns fals if the date is after mindate', () => {
component.mindate = new Date('2020-01-01')
component.day = 1
component.month = 1
component.year = 2025
expect(component.minError).toEqual(false)
})
})
describe('maxError', () => {
it('returns true if the date is after maxdate', () => {
component.maxdate = new Date('2020-01-01')
component.day = 1
component.month = 1
component.year = 2025
expect(component.maxError).toEqual(true)
})
it('returns false if the date is before maxdate', () => {
component.maxdate = new Date('2020-01-01')
component.day = 1
component.month = 1
component.year = 2005
expect(component.maxError).toEqual(false)
})
})
}) })

View File

@ -0,0 +1,105 @@
import Vue from 'vue'
import { mount } from '@vue/test-utils'
import PopDateRange from '../pop_date_range'
import { makeTestWrapper } from '../../test_utils/component_test_helpers'
const PopDateRangeWrapper = makeTestWrapper({
components: { PopDateRange },
templatePath: 'pop_date_range.html',
data: function() {
return {
initialMinStartDate: '2019-09-14',
initialMaxEndDate: '2022-09-14',
}
},
})
describe('PopDateRange Test', () => {
const component = new Vue(PopDateRange)
it('should calculate the max start date', () => {
component.maxStartDate = new Date('2020-01-01')
const date = new Date('2019-12-31')
expect(component.calcMaxStartDate(date)).toEqual(date)
})
it('should calculate the min end date', () => {
component.minEndDate = new Date('2020-01-01')
const date = new Date('2020-02-02')
expect(component.calcMinEndDate(date)).toEqual(date)
})
it('should add an error to the start date if it is out of range', () => {
const wrapper = mount(PopDateRangeWrapper, {
propsData: {
initialData: {},
},
})
const error = ['usa-input--error']
var startDateField = wrapper.find('fieldset[name="start_date"]')
var endDateField = wrapper.find('fieldset[name="end_date"]')
// set valid date range
startDateField.find('input[name="date-month"]').setValue('01')
startDateField.find('input[name="date-day"]').setValue('01')
startDateField.find('input[name="date-year"]').setValue('2020')
endDateField.find('input[name="date-month"]').setValue('01')
endDateField.find('input[name="date-day"]').setValue('01')
endDateField.find('input[name="date-year"]').setValue('2021')
// manually trigger the change event in the hidden fields
startDateField.find('input[name="start_date"]').trigger('change')
endDateField.find('input[name="end_date"]').trigger('change')
// check that both dates do not have error class
expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error))
expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error))
// update start date to be after end date and trigger change event
startDateField.find('input[name="date-year"]').setValue('2022')
startDateField.find('input[name="start_date"]').trigger('change')
expect(startDateField.classes()).toEqual(expect.arrayContaining(error))
expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error))
})
it('should add an error to the end date if it is out of range', () => {
const wrapper = mount(PopDateRangeWrapper, {
propsData: {
initialData: {},
},
})
const error = ['usa-input--error']
var startDateField = wrapper.find('fieldset[name="start_date"]')
var endDateField = wrapper.find('fieldset[name="end_date"]')
// set valid date range
startDateField.find('input[name="date-month"]').setValue('01')
startDateField.find('input[name="date-day"]').setValue('01')
startDateField.find('input[name="date-year"]').setValue('2020')
endDateField.find('input[name="date-month"]').setValue('01')
endDateField.find('input[name="date-day"]').setValue('01')
endDateField.find('input[name="date-year"]').setValue('2021')
// manually trigger the change event in the hidden fields
startDateField.find('input[name="start_date"]').trigger('change')
endDateField.find('input[name="end_date"]').trigger('change')
// check that both dates do not have error class
expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error))
expect(endDateField.classes()).toEqual(expect.not.arrayContaining(error))
// update end date to be before end date and trigger change event
endDateField.find('input[name="date-year"]').setValue('2019')
endDateField.find('input[name="end_date"]').trigger('change')
expect(startDateField.classes()).toEqual(expect.not.arrayContaining(error))
expect(endDateField.classes()).toEqual(expect.arrayContaining(error))
})
})

View File

@ -1,28 +1,22 @@
import * as R from 'ramda'
import { format } from 'date-fns'
import DateSelector from './date_selector'
import { emitEvent } from '../lib/emitters' import { emitEvent } from '../lib/emitters'
import Modal from '../mixins/modal' import Modal from '../mixins/modal'
import optionsinput from './options_input' import optionsinput from './options_input'
import textinput from './text_input' import textinput from './text_input'
import clindollaramount from './clin_dollar_amount' import clindollaramount from './clin_dollar_amount'
import PopDateRange from './pop_date_range'
const TOTAL_AMOUNT = 'total_amount' const TOTAL_AMOUNT = 'total_amount'
const OBLIGATED_AMOUNT = 'obligated_amount' const OBLIGATED_AMOUNT = 'obligated_amount'
const START_DATE = 'start_date'
const END_DATE = 'end_date'
const POP = 'period_of_performance'
const NUMBER = 'number' const NUMBER = 'number'
export default { export default {
name: 'clin-fields', name: 'clin-fields',
components: { components: {
DateSelector,
optionsinput, optionsinput,
textinput, textinput,
clindollaramount, clindollaramount,
PopDateRange,
}, },
mixins: [Modal], mixins: [Modal],
@ -37,79 +31,26 @@ export default {
type: Number, type: Number,
default: 0, default: 0,
}, },
initialStartDate: {
type: String,
default: null,
},
initialEndDate: {
type: String,
default: null,
},
initialClinNumber: { initialClinNumber: {
type: String, type: String,
default: null, default: null,
}, },
contractStart: {
type: String,
required: true,
},
contractEnd: {
type: String,
required: true,
},
}, },
data: function() { data: function() {
const start = !!this.initialStartDate
? new Date(this.initialStartDate)
: undefined
const end = !!this.initialEndDate
? new Date(this.initialEndDate)
: undefined
const fundingValidation = const fundingValidation =
this.initialObligated && this.initialTotal this.initialObligated && this.initialTotal
? this.initialObligated <= this.initialTotal ? this.initialObligated <= this.initialTotal
: true : true
const popValidation = !this.initialStartDate ? false : start < end
const clinNumber = !!this.initialClinNumber const clinNumber = !!this.initialClinNumber
? this.initialClinNumber ? this.initialClinNumber
: undefined : undefined
const contractStartDate = new Date(this.contractStart)
const contractEndDate = new Date(this.contractEnd)
return { return {
clinIndex: this.initialClinIndex, clinIndex: this.initialClinIndex,
clinNumber: clinNumber, clinNumber: clinNumber,
startDate: start,
endDate: end,
popValid: popValidation,
startDateValid: false,
endDateValid: false,
contractStartDate: contractStartDate,
contractEndDate: contractEndDate,
clinNumber: clinNumber, clinNumber: clinNumber,
showClin: true, showClin: true,
popErrors: [],
validations: [
{
func: this.popDateOrder,
message: 'PoP start date must be before end date.',
},
{
func: this.popStartsAfterContract,
message: `PoP start date must be on or after ${format(
contractStartDate,
'MMM D, YYYY'
)}.`,
},
{
func: this.popEndsBeforeContract,
message: `PoP end date must be before or on ${format(
contractEndDate,
'MMM D, YYYY'
)}.`,
},
],
totalAmount: this.initialTotal || 0, totalAmount: this.initialTotal || 0,
obligatedAmount: this.initialObligated || 0, obligatedAmount: this.initialObligated || 0,
fundingValid: fundingValidation, fundingValid: fundingValidation,
@ -127,11 +68,6 @@ export default {
obligatedAmount: this.initialObligated, obligatedAmount: this.initialObligated,
totalAmount: this.initialTotal, totalAmount: this.initialTotal,
}) })
emitEvent('field-mount', this, {
optional: false,
name: 'clins-' + this.clinIndex + '-' + POP,
valid: this.checkPopValid(),
})
}, },
methods: { methods: {
@ -143,50 +79,6 @@ export default {
}) })
}, },
checkPopValid: function() {
return (
this.popDateOrder() &&
this.popStartsAfterContract() &&
this.popEndsBeforeContract()
)
},
validatePop: function() {
this.popValid = this.checkPopValid()
emitEvent('field-change', this, {
name: 'clins-' + this.clinIndex + '-' + POP,
valid: this.popValid,
})
this.popErrors = R.pipe(
R.map(validation =>
!validation.func() ? validation.message : undefined
),
R.filter(Boolean)
)(this.validations)
},
popStartsAfterContract: function() {
if (this.startDateValid) {
return this.startDate >= this.contractStartDate
}
return true
},
popEndsBeforeContract: function() {
if (this.endDateValid) {
return this.endDate <= this.contractEndDate
}
return true
},
popDateOrder: function() {
if (!!this.startDate && !!this.endDate) {
return this.startDate < this.endDate
}
return true
},
checkFundingValid: function() { checkFundingValid: function() {
return this.obligatedAmount <= this.totalAmount return this.obligatedAmount <= this.totalAmount
}, },
@ -205,14 +97,6 @@ export default {
} else if (event.name.includes(OBLIGATED_AMOUNT)) { } else if (event.name.includes(OBLIGATED_AMOUNT)) {
this.obligatedAmount = parseFloat(event.value) this.obligatedAmount = parseFloat(event.value)
this.validateFunding() this.validateFunding()
} else if (event.name.includes(START_DATE)) {
if (!!event.value) this.startDate = new Date(event.value)
if (!!event.valid) this.startDateValid = event.valid
this.validatePop()
} else if (event.name.includes(END_DATE)) {
if (!!event.value) this.endDate = new Date(event.value)
if (!!event.valid) this.endDateValid = event.valid
this.validatePop()
} else if (event.name.includes(NUMBER)) { } else if (event.name.includes(NUMBER)) {
this.clinNumber = event.value this.clinNumber = event.value
} }

View File

@ -2,7 +2,7 @@ import Vue from 'vue'
import { getDaysInMonth } from 'date-fns' import { getDaysInMonth } from 'date-fns'
import { emitEvent } from '../lib/emitters' import { emitEvent } from '../lib/emitters'
var paddedNumber = function(number) { let paddedNumber = function(number) {
if ((number + '').length === 1) { if ((number + '').length === 1) {
return `0${number}` return `0${number}`
} else { } else {
@ -20,10 +20,6 @@ export default {
mindate: { type: String }, mindate: { type: String },
maxdate: { type: String }, maxdate: { type: String },
nameTag: { type: String }, nameTag: { type: String },
watch: {
type: Boolean,
default: false,
},
optional: { optional: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -36,7 +32,6 @@ export default {
month: this.initialmonth, month: this.initialmonth,
year: this.initialyear, year: this.initialyear,
name: this.nameTag, name: this.nameTag,
showValidation: false,
} }
}, },
@ -87,23 +82,29 @@ export default {
}, },
isMonthValid: function() { isMonthValid: function() {
var _month = parseInt(this.month) let _month = parseInt(this.month)
var valid = _month >= 0 && _month <= 12 let valid = _month >= 0 && _month <= 12
this._emitChange('month', this.month, valid)
return valid return valid
}, },
isDayValid: function() { isDayValid: function() {
var _day = parseInt(this.day) let _day = parseInt(this.day)
var valid = _day >= 0 && _day <= this.daysMaxCalculation let valid = _day >= 0 && _day <= this.daysMaxCalculation
this._emitChange('day', this.day, valid)
return valid return valid
}, },
isYearValid: function() { isYearValid: function() {
// Emit a change event // Emit a change event
var valid = parseInt(this.year) >= 1 let valid
this._emitChange('year', this.year, valid) let minYear = this.mindate ? this.minDateParsed.getFullYear() : null
let maxYear = this.maxdate ? this.maxDateParsed.getFullYear() : null
if (minYear && maxYear) {
valid = this.year >= minYear && this.year <= maxYear
} else {
valid = parseInt(this.year) >= 1
}
return valid return valid
}, },
@ -135,6 +136,10 @@ export default {
) )
}, },
isDateComplete: function() {
return !!this.day && !!this.month && !!this.year && this.year > 999
},
daysMaxCalculation: function() { daysMaxCalculation: function() {
switch (parseInt(this.month)) { switch (parseInt(this.month)) {
case 2: // February case 2: // February
@ -157,22 +162,49 @@ export default {
return 31 return 31
} }
}, },
minError: function() {
if (this.isDateComplete) {
return this.minDateParsed > this.dateParsed
}
return false
},
maxError: function() {
if (this.isDateComplete) {
return this.maxDateParsed < this.dateParsed
}
return false
},
maxDateParsed: function() {
return new Date(this.maxdate)
},
minDateParsed: function() {
return new Date(this.mindate)
},
dateParsed: function() {
return new Date(this.formattedDate)
},
}, },
methods: { methods: {
onInput: function(e) { onInput: function(e) {
this.showValidation = true
emitEvent('field-change', this, { emitEvent('field-change', this, {
value: this.formattedDate, value: this.formattedDate,
name: this.name, name: this.name,
watch: this.watch,
valid: this.isDateValid, valid: this.isDateValid,
}) })
},
_emitChange: function(name, value, valid) { this.$emit('date-change', {
emitEvent('field-change', this, { value, name, valid }) value: this.formattedDate,
name: this.name,
valid: this.isDateValid,
})
}, },
}, },

View File

@ -0,0 +1,88 @@
import { format } from 'date-fns'
import DateSelector from './date_selector'
const START_DATE = 'start_date'
const END_DATE = 'end_date'
export default {
name: 'pop-date-range',
components: {
DateSelector,
},
props: {
initialMinStartDate: String,
initialMaxEndDate: String,
initialStartDate: {
type: String,
default: null,
},
initialEndDate: {
type: String,
default: null,
},
clinIndex: Number,
},
data: function() {
let start = !!this.initialStartDate
? new Date(this.initialStartDate)
: false
let contractStart = new Date(this.initialMinStartDate)
let minEndDate = start && start > contractStart ? start : contractStart
let end = !!this.initialEndDate ? new Date(this.initialEndDate) : false
let contractEnd = new Date(this.initialMaxEndDate)
let maxStartDate = end && end < contractEnd ? end : contractEnd
// the maxStartDate and minEndDate change based on user input:
// the latest date the start can be is the PoP end date
// the earliest date the end can be is the PoP start date
// if the form is initialized with out a PoP, the maxStartDate and minEndDate
// default to the contract dates
return {
maxStartDate: maxStartDate,
minEndDate: minEndDate,
}
},
methods: {
handleDateChange: function(event) {
if (event.name.includes(START_DATE) && event.valid) {
let date = new Date(event.value)
this.minEndDate = this.calcMinEndDate(date)
} else if (event.name.includes(END_DATE) && event.valid) {
let date = new Date(event.value)
this.maxStartDate = this.calcMaxStartDate(date)
}
},
calcMaxStartDate: function(date) {
if (!!date && date < this.maxStartDate) {
return date
} else {
return this.maxStartDate
}
},
calcMinEndDate: function(date) {
if (!!date && date > this.minEndDate) {
return date
} else {
return this.minEndDate
}
},
},
computed: {
maxStartProp: function() {
return format(this.maxStartDate, 'YYYY-MM-DD')
},
minEndProp: function() {
return format(this.minEndDate, 'YYYY-MM-DD')
},
},
}

View File

@ -32,6 +32,7 @@ import NewEnvironment from './components/forms/new_environment'
import SemiCollapsibleText from './components/semi_collapsible_text' import SemiCollapsibleText from './components/semi_collapsible_text'
import ToForm from './components/forms/to_form' import ToForm from './components/forms/to_form'
import ClinFields from './components/clin_fields' import ClinFields from './components/clin_fields'
import PopDateRange from './components/pop_date_range'
Vue.config.productionTip = false Vue.config.productionTip = false
@ -65,6 +66,7 @@ const app = new Vue({
SemiCollapsibleText, SemiCollapsibleText,
ToForm, ToForm,
ClinFields, ClinFields,
PopDateRange,
}, },
mounted: function() { mounted: function() {

View File

@ -311,121 +311,142 @@
</div> </div>
</div> </div>
<div class="form-row"> <pop-date-range
<div class="form-col"> initial-min-start-date="2019-09-14"
<date-selector initial-max-end-date="2022-09-14"
v-bind:clin-index="clinIndex"
name-tag='start_date'
initialmonth=""
initialday="" initial-start-date="None"
initialyear=""
v-bind:watch='true'
:optional='false' initial-end-date="None"
inline-template>
inline-template>
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && showValidation }"> <div>
<legend> <div class="form-row">
<div class="usa-input__title"> <div class="form-col">
Start Date <date-selector
</div> :mindate="initialMinStartDate"
:maxdate="maxStartProp"
<p class='usa-input__help'>
For example: 07 04 1776
</p>
</legend>
<div class="date-picker-component">
<input name="start_date" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>Month</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>Day</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
id="date-year"
maxlength="4"
type="number"
v-model="year"
name-tag='start_date'
initialmonth=""
initialday=""
initialyear=""
v-on:change="onInput" :optional='false'
/> v-on:date-change='handleDateChange'
inline-template>
</div> <fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
Start Date
</div>
<div v-if="showValidation"> <p class='usa-input__help'>
<div class="usa-form-group-date-ok" v-if="isDateValid"> For example: 07 04 1776
</p>
<div v-if='minError' class="usa-input-error-message">
PoP start date must be on or after September 14, 2019.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP start date must be before end date.
</div>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>Month</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>Day</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
max="2022"
min="2019"
v-on:change="onInput"
/>
</div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid">
<span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span> <span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
</div> </div>
<div class="usa-form-group-date-ok" v-else> <div class="usa-form-group-date-ok" v-else>
<span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e"> <span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/> <path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/>
</svg> </svg>
</span> </span>
</div> </div>
</div>
</div>
</fieldset>
</date-selector>
</div> </div>
</div> </div>
</fieldset>
</date-selector>
</div>
</div>
<div class="form-row">
<div class="form-col">
<date-selector
name-tag='end_date'
initialmonth=""
initialday=""
initialyear=""
v-bind:watch='true'
:optional='false'
inline-template>
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && showValidation }"> <div class="form-row">
<legend> <div class="form-col">
<div class="usa-input__title"> <date-selector
End Date :mindate="minEndProp"
</div> :maxdate="initialMaxEndDate"
name-tag='end_date'
initialmonth=""
initialday=""
initialyear=""
:optional='false'
v-on:date-change='handleDateChange'
inline-template>
<fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
End Date
</div>
@ -447,87 +468,85 @@
</div> </div>
</div> </div>
<p class='usa-input__help'>
<p class='usa-input__help'> For example: 07 04 1776
For example: 07 04 1776 </p>
</p>
</legend>
<div class="date-picker-component"> <div v-if='minError' class="usa-input-error-message">
<input name="end_date" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" /> PoP end date must be after start date.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP end date must be on or after September 14, 2022.
</div>
</legend>
<div class="usa-form-group usa-form-group-month"> <div class="date-picker-component">
<label>Month</label> <input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day"> <div class="usa-form-group usa-form-group-month">
<label>Day</label> <label>Month</label>
<input <input
name="date-day" name="date-month"
maxlength="2" max="12"
min="1" maxlength="2"
type="number" min="1"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }" type="number"
v-bind:max="daysMaxCalculation" v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="day" v-model="month"
v-on:change="onInput" v-on:change="onInput"
/> />
</div> </div>
<div class="usa-form-group usa-form-group-year"> <div class="usa-form-group usa-form-group-day">
<label>Year</label> <label>Day</label>
<input <input
id="date-year" name="date-day"
maxlength="4" maxlength="2"
type="number" min="1"
v-model="year" type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-on:change="onInput" v-model="day"
/> v-on:change="onInput"
/>
</div>
</div> <div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
v-on:change="onInput"
/>
</div>
<div v-if="showValidation"> <div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid"> <div class="usa-form-group-date-ok" v-if="isDateValid">
<span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span> <span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
</div> </div>
<div class="usa-form-group-date-ok" v-else> <div class="usa-form-group-date-ok" v-else>
<span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e"> <span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/> <path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/>
</svg> </svg>
</span> </span>
</div> </div>
</div>
</div>
</fieldset>
</date-selector>
</div> </div>
</div> </div>
</fieldset> </div>
</date-selector> </pop-date-range>
</div>
</div>
<div class="form-row">
<div class="usa-input-error-message form-has-errors">
<p v-for="error in popErrors" :key="error" v-html='error'></p>
</div>
</div>
</div> </div>
<div v-show="$root.activeModal === removeModalId" v-cloak> <div v-show="$root.activeModal === removeModalId" v-cloak>

View File

@ -0,0 +1,234 @@
<pop-date-range
initial-min-start-date="2019-09-14"
initial-max-end-date="2022-09-14"
v-bind:clin-index="1"
initial-start-date="None"
initial-end-date="None"
inline-template>
<div>
<div class="form-row">
<div class="form-col">
<date-selector
:mindate="initialMinStartDate"
:maxdate="maxStartProp"
name-tag='start_date'
initialmonth=""
initialday=""
initialyear=""
:optional='true'
v-on:date-change='handleDateChange'
inline-template>
<fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
Start Date
</div>
<p class='usa-input__help'>
For example: 07 04 1776
</p>
<div v-if='minError' class="usa-input-error-message">
PoP start date must be on or after September 14, 2019.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP start date must be before end date.
</div>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>Month</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>Day</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
max="2022"
min="2019"
v-on:change="onInput"
/>
</div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid">
<span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
</div>
<div class="usa-form-group-date-ok" v-else>
<span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/>
</svg>
</span>
</div>
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
<div class="form-row">
<div class="form-col">
<date-selector
:mindate="minEndProp"
:maxdate="initialMaxEndDate"
name-tag='end_date'
initialmonth=""
initialday=""
initialyear=""
:optional='true'
v-on:date-change='handleDateChange'
inline-template>
<fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
End Date
</div>
<div class='usa-alert usa-alert-info' role='alert' aria-live='polite'>
<div class='usa-alert-body'>
<p class='usa-alert-text'>
A CLIN's period of performance must end before September 14, 2022.
</p>
</div>
</div>
<p class='usa-input__help'>
For example: 07 04 1776
</p>
<div v-if='minError' class="usa-input-error-message">
PoP end date must be after start date.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP end date must be on or after September 14, 2022.
</div>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>Month</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>Day</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>Year</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
v-on:change="onInput"
/>
</div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid">
<span class="icon icon--ok icon--green" aria-hidden="true"><svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="check-circle" class="svg-inline--fa fa-check-circle fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z"></path></svg></span>
</div>
<div class="usa-form-group-date-ok" v-else>
<span class="icon icon--alert icon--red" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="#fdb81e">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"/>
</svg>
</span>
</div>
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
</div>
</pop-date-range>

View File

@ -199,17 +199,25 @@
Period of Performance Period of Performance
</div> </div>
</div> </div>
<pop-date-range initial-max-end-date="2022-09-14" initial-min-start-date="2019-09-14" inline-template="" v-bind:clin-index="clinIndex">
<div>
<div class="form-row"> <div class="form-row">
<div class="form-col"> <div class="form-col">
<date-selector :name-tag="'clins-' + clinIndex + '-start_date'" :optional="false" :watch="true" inline-template=""> <date-selector :maxdate="maxStartProp" :mindate="initialMinStartDate" :name-tag="'clins-' + clinIndex + '-start_date'" :optional="false" inline-template="" v-on:date-change="handleDateChange">
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid &amp;&amp; showValidation }"> <fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid &amp;&amp; isDateComplete, 'usa-input--error': !isDateValid &amp;&amp; isDateComplete }">
<legend> <legend>
<div class="usa-input__title"> <div class="usa-input__title">
Start Date Start Date
</div> </div>
<p class="usa-input__help"> <p class="usa-input__help">
For example: 07 04 1776 For example: 07 04 1776
</p> </p>
<div class="usa-input-error-message" v-if="minError">
PoP start date must be on or after September 14, 2019.
</div>
<div class="usa-input-error-message" v-if="maxError">
PoP start date must be before end date.
</div>
</legend> </legend>
<div class="date-picker-component"> <div class="date-picker-component">
<input :name="name" type="hidden" v-bind:value="formattedDate" v-on:change="onInput"/> <input :name="name" type="hidden" v-bind:value="formattedDate" v-on:change="onInput"/>
@ -223,11 +231,19 @@
</div> </div>
<div class="usa-form-group usa-form-group-year"> <div class="usa-form-group usa-form-group-year">
<label>Year</label> <label>Year</label>
<input maxlength="4" name="date-year" type="number" v-model="year" v-on:change="onInput"/> <input max="2022" maxlength="4" min="2019" name="date-year" type="number" v-model="year" v-on:change="onInput"/>
</div> </div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid"> <div class="usa-form-group-date-ok" v-if="isDateValid">
<span aria-hidden="true" class="icon icon--ok icon--green"><svg aria-hidden="true" class="svg-inline--fa fa-check-circle fa-w-16" data-icon="check-circle" data-prefix="fas" focusable="false" role="img" viewbox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z" fill="currentColor"></path></svg></span> <span aria-hidden="true" class="icon icon--ok icon--green"><svg aria-hidden="true" class="svg-inline--fa fa-check-circle fa-w-16" data-icon="check-circle" data-prefix="fas" focusable="false" role="img" viewbox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z" fill="currentColor"></path></svg></span>
</div> </div>
<div class="usa-form-group-date-ok" v-else="">
<span aria-hidden="true" class="icon icon--alert icon--red"><svg fill="#fdb81e" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"></path>
</svg>
</span>
</div>
</div>
</div> </div>
</fieldset> </fieldset>
</date-selector> </date-selector>
@ -235,12 +251,12 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<div class="form-col"> <div class="form-col">
<date-selector :name-tag="'clins-' + clinIndex + '-end_date'" :optional="false" :watch="true" inline-template=""> <date-selector :maxdate="initialMaxEndDate" :mindate="minEndProp" :name-tag="'clins-' + clinIndex + '-end_date'" :optional="false" inline-template="" v-on:date-change="handleDateChange">
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid &amp;&amp; showValidation }"> <fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid &amp;&amp; isDateComplete, 'usa-input--error': !isDateValid &amp;&amp; isDateComplete }">
<legend> <legend>
<div class="usa-input__title"> <div class="usa-input__title">
End Date End Date
</div> </div>
<div aria-live="polite" class="usa-alert usa-alert-info" role="alert"> <div aria-live="polite" class="usa-alert usa-alert-info" role="alert">
<div class="usa-alert-body"> <div class="usa-alert-body">
<p class="usa-alert-text"> <p class="usa-alert-text">
@ -249,8 +265,14 @@
</div> </div>
</div> </div>
<p class="usa-input__help"> <p class="usa-input__help">
For example: 07 04 1776 For example: 07 04 1776
</p> </p>
<div class="usa-input-error-message" v-if="minError">
PoP end date must be after start date.
</div>
<div class="usa-input-error-message" v-if="maxError">
PoP end date must be on or after September 14, 2022.
</div>
</legend> </legend>
<div class="date-picker-component"> <div class="date-picker-component">
<input :name="name" type="hidden" v-bind:value="formattedDate" v-on:change="onInput"/> <input :name="name" type="hidden" v-bind:value="formattedDate" v-on:change="onInput"/>
@ -266,19 +288,24 @@
<label>Year</label> <label>Year</label>
<input maxlength="4" name="date-year" type="number" v-model="year" v-on:change="onInput"/> <input maxlength="4" name="date-year" type="number" v-model="year" v-on:change="onInput"/>
</div> </div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid"> <div class="usa-form-group-date-ok" v-if="isDateValid">
<span aria-hidden="true" class="icon icon--ok icon--green"><svg aria-hidden="true" class="svg-inline--fa fa-check-circle fa-w-16" data-icon="check-circle" data-prefix="fas" focusable="false" role="img" viewbox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z" fill="currentColor"></path></svg></span> <span aria-hidden="true" class="icon icon--ok icon--green"><svg aria-hidden="true" class="svg-inline--fa fa-check-circle fa-w-16" data-icon="check-circle" data-prefix="fas" focusable="false" role="img" viewbox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><path d="M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zM227.314 387.314l184-184c6.248-6.248 6.248-16.379 0-22.627l-22.627-22.627c-6.248-6.249-16.379-6.249-22.628 0L216 308.118l-70.059-70.059c-6.248-6.248-16.379-6.248-22.628 0l-22.627 22.627c-6.248 6.248-6.248 16.379 0 22.627l104 104c6.249 6.249 16.379 6.249 22.628.001z" fill="currentColor"></path></svg></span>
</div> </div>
<div class="usa-form-group-date-ok" v-else="">
<span aria-hidden="true" class="icon icon--alert icon--red"><svg fill="#fdb81e" viewbox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
<path d="M8 16c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8zM8 2C4.691 2 2 4.691 2 8s2.691 6 6 6 6-2.691 6-6-2.691-6-6-6zm0 8c-.552 0-1-.447-1-1V4c0-.552.448-1 1-1s1 .448 1 1v5c0 .553-.448 1-1 1zm0 3c-.26 0-.52-.11-.71-.29-.18-.19-.29-.45-.29-.71 0-.271.11-.521.29-.71.38-.37 1.05-.37 1.42 0 .18.189.29.45.29.71s-.11.52-.29.71c-.19.18-.45.29-.71.29z"></path>
</svg>
</span>
</div>
</div>
</div> </div>
</fieldset> </fieldset>
</date-selector> </date-selector>
</div> </div>
</div> </div>
<div class="form-row">
<div class="usa-input-error-message form-has-errors">
<p :key="error" v-for="error in popErrors" v-html="error"></p>
</div>
</div> </div>
</pop-date-range>
</div> </div>
<div v-cloak="" v-show="$root.activeModal === removeModalId"> <div v-cloak="" v-show="$root.activeModal === removeModalId">
<div :id='"modal--" + removeModalId' class="modal modal--dismissable"> <div :id='"modal--" + removeModalId' class="modal modal--dismissable">

View File

@ -1,9 +1,9 @@
{% from "components/clin_dollar_amount.html" import CLINDollarAmount %} {% from "components/clin_dollar_amount.html" import CLINDollarAmount %}
{% from 'components/alert.html' import Alert %} {% from 'components/alert.html' import Alert %}
{% from 'components/date_picker.html' import DatePicker %}
{% from 'components/icon.html' import Icon %} {% from 'components/icon.html' import Icon %}
{% from 'components/options_input.html' import OptionsInput %} {% from 'components/options_input.html' import OptionsInput %}
{% from 'components/text_input.html' import TextInput %} {% from 'components/text_input.html' import TextInput %}
{% from 'components/pop_date_range.html' import PopDateRange %}
{% macro CLINFields(contract_start, contract_end, fields=None, index=None) %} {% macro CLINFields(contract_start, contract_end, fields=None, index=None) %}
<clin-fields <clin-fields
@ -128,156 +128,11 @@
{{ 'task_orders.form.pop' | translate }} {{ 'task_orders.form.pop' | translate }}
</div> </div>
</div> </div>
{% set contract_end_formatted = contract_end | dateFromString(formatter="%Y-%m-%d") | formattedDate(formatter="%B %d, %Y") %}
{% if fields %} {% if fields %}
<div class="form-row"> {{ PopDateRange(start_field=fields.start_date, end_field=fields.end_date, optional=False, mindate=contract_start, maxdate=contract_end) }}
<div class="form-col">
{{ DatePicker(fields.start_date, watch=True, optional=False) }}
</div>
</div>
<div class="form-row">
<div class="form-col">
{% call DatePicker(fields.end_date, watch=True, optional=False) %}
{{ Alert(message="task_orders.form.pop_end_alert" | translate({'end_date': contract_end_formatted})) }}
{% endcall %}
</div>
</div>
{% else %} {% else %}
<div class="form-row"> {{ PopDateRange(optional=False, mindate=contract_start, maxdate=contract_end) }}
<div class="form-col">
<date-selector :name-tag="'clins-' + clinIndex + '-start_date'" :watch='true' :optional='false' inline-template>
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && showValidation }">
<legend>
<div class="usa-input__title">
{{ 'task_orders.form.pop_start' | translate }}
</div>
<p class='usa-input__help'>
{{ 'task_orders.form.pop_example' | translate }}
</p>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>{{ 'components.date_selector.month' | translate }}</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>{{ 'components.date_selector.day' | translate }}</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>{{ 'components.date_selector.year' | translate }}</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }}
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
<div class="form-row">
<div class="form-col">
<date-selector :name-tag="'clins-' + clinIndex + '-end_date'" :watch='true' :optional='false' inline-template>
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && showValidation }">
<legend>
<div class="usa-input__title">
{{ 'task_orders.form.pop_end' | translate }}
</div>
{{ Alert(message="task_orders.form.pop_end_alert" | translate({'end_date': contract_end_formatted})) }}
<p class='usa-input__help'>
{{ 'task_orders.form.pop_example' | translate }}
</p>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>{{ 'components.date_selector.month' | translate }}</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>{{ 'components.date_selector.day' | translate }}</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>{{ 'components.date_selector.year' | translate }}</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }}
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
{% endif %} {% endif %}
<div class="form-row">
<div class="usa-input-error-message form-has-errors">
<p v-for="error in popErrors" :key="error" v-html='error'></p>
</div>
</div>
</div> </div>
<div v-show="$root.activeModal === removeModalId" v-cloak> <div v-show="$root.activeModal === removeModalId" v-cloak>

View File

@ -7,7 +7,6 @@
description=field.description, description=field.description,
mindate=None, mindate=None,
maxdate=None, maxdate=None,
watch=False,
optional=True) -%} optional=True) -%}
<date-selector <date-selector
@ -17,11 +16,10 @@
initialmonth="{{ field.data.month }}" initialmonth="{{ field.data.month }}"
initialday="{{ field.data.day }}" initialday="{{ field.data.day }}"
initialyear="{{ field.data.year }}" initialyear="{{ field.data.year }}"
v-bind:watch='{{ watch | string | lower }}'
:optional='{{ optional | string | lower }}' :optional='{{ optional | string | lower }}'
inline-template> inline-template>
<fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && showValidation }"> <fieldset class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete }">
<legend> <legend>
<div class="usa-input__title"> <div class="usa-input__title">
{{ label }} {{ label }}
@ -83,7 +81,7 @@
</div> </div>
<div v-if="showValidation"> <div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid"> <div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }} {{ Icon("ok", classes="icon--green") }}
</div> </div>

View File

@ -0,0 +1,210 @@
{% from 'components/alert.html' import Alert %}
{% from 'components/icon.html' import Icon %}
{% macro PopDateRange(start_field=None, end_field=None, mindate=mindate, maxdate=maxdate, optional=True, index=None) %}
<pop-date-range
initial-min-start-date="{{ mindate }}"
initial-max-end-date="{{ maxdate }}"
{% if index %}
v-bind:clin-index="{{ index }}"
{% else %}
v-bind:clin-index="clinIndex"
{% endif %}
{% if start_field %}
initial-start-date="{{ start_field.data }}"
{% endif %}
{% if end_field %}
initial-end-date="{{ end_field.data }}"
{% endif %}
inline-template>
<div>
<div class="form-row">
<div class="form-col">
<date-selector
:mindate="initialMinStartDate"
:maxdate="maxStartProp"
{% if start_field %}
name-tag='{{ start_field.name }}'
initialmonth="{{ start_field.data.month }}"
initialday="{{ start_field.data.day }}"
initialyear="{{ start_field.data.year }}"
{% else %}
:name-tag="'clins-' + clinIndex + '-start_date'"
{% endif %}
:optional='{{ optional | string | lower }}'
v-on:date-change='handleDateChange'
inline-template>
<fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
{{ "task_orders.form.pop_start" | translate }}
</div>
<p class='usa-input__help'>
{{ "task_orders.form.pop_example" | translate | safe }}
</p>
<div v-if='minError' class="usa-input-error-message">
PoP start date must be on or after {{ mindate | formattedDate(formatter="%B %d, %Y") }}.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP start date must be before end date.
</div>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>{{ 'components.date_selector.month' | translate }}</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>{{ 'components.date_selector.day' | translate }}</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>{{ 'components.date_selector.year' | translate }}</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
{% if maxdate %}max="{{ maxdate.year }}"{% endif %}
{% if mindate %}min="{{ mindate.year }}"{% endif %}
v-on:change="onInput"
/>
</div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }}
</div>
<div class="usa-form-group-date-ok" v-else>
{{ Icon("alert", classes="icon--red")}}
</div>
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
<div class="form-row">
<div class="form-col">
<date-selector
:mindate="minEndProp"
:maxdate="initialMaxEndDate"
{% if end_field %}
name-tag='{{ end_field.name }}'
initialmonth="{{ end_field.data.month }}"
initialday="{{ end_field.data.day }}"
initialyear="{{ end_field.data.year }}"
{% else %}
:name-tag="'clins-' + clinIndex + '-end_date'"
{% endif %}
:optional='{{ optional | string | lower }}'
v-on:date-change='handleDateChange'
inline-template>
<fieldset :name="name" class="usa-input date-picker" v-bind:class="{ 'usa-input--success': isDateValid && isDateComplete, 'usa-input--error': !isDateValid && isDateComplete }">
<legend>
<div class="usa-input__title">
{{ 'task_orders.form.pop_end' | translate }}
</div>
{% set formatted_end_date = maxdate | formattedDate(formatter="%B %d, %Y") %}
{{ Alert(message="task_orders.form.pop_end_alert" | translate({'end_date': formatted_end_date })) }}
<p class='usa-input__help'>
{{ 'task_orders.form.pop_example' | translate }}
</p>
<div v-if='minError' class="usa-input-error-message">
PoP end date must be after start date.
</div>
<div v-if='maxError' class="usa-input-error-message">
PoP end date must be on or after {{ formatted_end_date }}.
</div>
</legend>
<div class="date-picker-component">
<input :name="name" v-bind:value="formattedDate" v-on:change="onInput" type="hidden" />
<div class="usa-form-group usa-form-group-month">
<label>{{ 'components.date_selector.month' | translate }}</label>
<input
name="date-month"
max="12"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (month && !isMonthValid) }"
v-model="month"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-day">
<label>{{ 'components.date_selector.day' | translate }}</label>
<input
name="date-day"
maxlength="2"
min="1"
type="number"
v-bind:class="{ 'usa-input-error': (day && !isDayValid) }"
v-bind:max="daysMaxCalculation"
v-model="day"
v-on:change="onInput"
/>
</div>
<div class="usa-form-group usa-form-group-year">
<label>{{ 'components.date_selector.year' | translate }}</label>
<input
name="date-year"
maxlength="4"
type="number"
v-model="year"
v-on:change="onInput"
/>
</div>
<div v-if="isDateComplete">
<div class="usa-form-group-date-ok" v-if="isDateValid">
{{ Icon("ok", classes="icon--green") }}
</div>
<div class="usa-form-group-date-ok" v-else>
{{ Icon("alert", classes="icon--red")}}
</div>
</div>
</div>
</fieldset>
</date-selector>
</div>
</div>
</div>
</pop-date-range>
{% endmacro %}

View File

@ -38,12 +38,8 @@ def test_clin_form_start_date_before_end_date():
def test_clin_form_pop_dates_within_contract_dates(): def test_clin_form_pop_dates_within_contract_dates():
CONTRACT_START_DATE = datetime.datetime.strptime( CONTRACT_START_DATE = app.config.get("CONTRACT_START_DATE")
app.config.get("CONTRACT_START_DATE"), "%Y-%m-%d" CONTRACT_END_DATE = app.config.get("CONTRACT_END_DATE")
).date()
CONTRACT_END_DATE = datetime.datetime.strptime(
app.config.get("CONTRACT_END_DATE"), "%Y-%m-%d"
).date()
invalid_start = CONTRACT_START_DATE - relativedelta(months=1) invalid_start = CONTRACT_START_DATE - relativedelta(months=1)
invalid_end = CONTRACT_END_DATE + relativedelta(months=1) invalid_end = CONTRACT_END_DATE + relativedelta(months=1)

View File

@ -135,3 +135,17 @@ def test_make_clin_fields(env, app):
0, 0,
) )
write_template(clin_fields, "clin_fields.html") write_template(clin_fields, "clin_fields.html")
def test_make_pop_date_range(env, app):
pop_date_range_template = env.get_template("components/pop_date_range.html")
pop_date_range_macro = getattr(pop_date_range_template.module, "PopDateRange")
form = CLINForm()
pop_date_range = pop_date_range_macro(
start_field=form.start_date,
end_field=form.end_date,
mindate=app.config.get("CONTRACT_START_DATE"),
maxdate=app.config.get("CONTRACT_END_DATE"),
index=1,
)
write_template(pop_date_range, "pop_date_range.html")