gaf-mobile
Version:
GAF mobile Web site
688 lines (607 loc) • 24.5 kB
JavaScript
module('gafMobileApp')
.controller('PostProjectCtrl', function($scope, $location, $route,
$q, JobBundles, ProjectLocation, Projects, Budgets, Auth, Contests,
Analytics, Categories, Users, currencyFilter, ProjectTemplates, Deposits,
Translations, CookieStore, TEMPLATEQS_TRANSLATION_DOMAIN,
TEMPLATEANS_TRANSLATION_DOMAIN, LANGUAGE_COOKIE, DEFAULT_PROJECT_CONFIG,
FIVE_EYES, Experiments) {
var _this = this;
var preloads = [];
// TODO: Fix setting defaults until feature for loading
// these configs is implemented
_this.defaultProjectConfig = DEFAULT_PROJECT_CONFIG;
// Help modal
_this.modal = {};
// Load the current project (or create a new one)
_this.currentProject = Projects.getCurrent();
// Export the project's details on the scope
_this.project = _this.currentProject.get();
// In repost mode we don't show the dropdowns
_this.isRepost = _this.currentProject.get().jobs &&
_this.currentProject.get().jobs.length &&
!_this.currentProject.get().category;
_this.showProjectOption = false;
_this.state = 'start';
_this.loggedInUser = $route.current.locals.user;
// Clean up when SetTitleFromAnswer A/B test is done
if (Auth.isAuthenticated()) {
var loggedInUserId = Auth.getUserId();
_this.setTitleFromAnswer = Experiments.isInSplit(loggedInUserId,
'SetTitleFromAnswer', 0.5);
}
// Clean up when RequireProjectDescription A/B test is done
if (_this.project.type !== 'contest') {
if (_this.loggedInUser && _this.loggedInUser.get &&
_this.loggedInUser.get().location) {
var userLocation = _this.loggedInUser.get().location.country.name;
_this.isFromNonFiveEye = FIVE_EYES.indexOf(userLocation) === -1 ?
Experiments.activateTest('RequireProjectDescription') : false;
} else {
ProjectLocation.getLocation().then(function(place) {
var userLocation = ProjectLocation.denormaliseLatLng(place) || {};
_this.isFromNonFiveEye =
FIVE_EYES.indexOf(userLocation.country) === -1 ?
Experiments.activateTest('RequireProjectDescription') : false;
});
}
}
// set default `duration` if not yet set (only contests use `duration`)
_this.project.duration = _this.project.duration || 7;
// Set budget list for project types
_this.budgets = {
fixed: [],
hourly: []
};
// Set default title & jobs if empty based on the search params
if (!_this.currentProject.get().title && ($location.search().title ||
$location.search().project_title)) {
var defaultTitle = decodeURIComponent($location.search().title ||
$location.search().project_title);
_this.currentProject.setTitle(defaultTitle);
}
if (!_this.currentProject.get().jobs && $location.search().jobs) {
_this.currentProject.setJobs($location.search().jobs.split(','));
}
_this.isFetchingLocation = ProjectLocation.isFetchingLocation;
_this.locationValid = function() {
return ProjectLocation.isValidPlace(_this.project.place);
};
_this.getLocation = function() {
_this.error = {};
ProjectLocation.getLocation().then(function(place) {
_this.project.place = place;
}).catch(function() {
_this.error.locationError = true;
});
};
// Helper for handling the 'Post Project' logic
// Clean up postFromLoggedOut when SetTitleFromAnswer A/B test is done
var postProject = function(project, postFromLoggedOut) {
if (Auth.isAuthenticated()) {
// Clear any previous errors
_this.error = {};
return Projects.post(project)
.then(function(newProject) {
Analytics.trackAction('project', 'post', 'SUCCESS');
// Reset post project funnel
_this.project = newProject.get();
project.reset();
// Clean up when SetTitleFromAnswer A/B test is done
if (!postFromLoggedOut) {
Experiments.activateTest('SetTitleFromAnswer', loggedInUserId);
}
Analytics.trackGTMConversion('project', _this.project.id,
'MBW NonHireMe Posted');
$location.url('/projects/project-' + _this.project.id + '#bids');
}).catch(function(error) {
_this.currentProject.markAsReady(false);
// If failure, show the 'project details' view with
// an error message
_this.state = 'start';
if (error.code === 'UNVERIFIED_PAYMENT') {
Analytics.trackAction('project', 'post', error.code);
_this.currentProject.markAsReady(true);
$location.url('payments/verify?postProject=true&return=' +
$location.url());
} else if (error.code === 'NEGATIVE_BALANCE') {
Analytics.trackAction('project', 'post', error.code);
_this.error.negativeBalance = true;
} else {
Analytics.trackError('project', 'post', error.code, error.data);
_this.error.internalError = error.code;
}
});
} else {
$location.url('/signup?role=employer&return=' +
encodeURIComponent($location.url()));
return $q.reject();
}
};
_this.postContest = function(contest, draft) {
if (Auth.isAuthenticated()) {
if (contest.jobs && contest.jobs.length > 0 &&
angular.isDefined(contest.jobs[0].id)) {
contest.jobs = contest.jobs.map(function(job) {
return job.id;
});
}
_this.error = {};
var newContest = {
title: contest.title,
description: contest.description,
currency_id: contest.currency.id,
prize: contest.budget,
job_ids: contest.jobs,
duration: contest.duration
};
return Contests.post(newContest)
.then(function(project) {
_this.error = {};
_this.project.id = project.get().id;
_this.contestPostedLive = true;
_this.state = 'success';
_this.showPaymentReleaseModal = false;
Analytics.trackGTMConversion('contest', _this.project.id,
'MBW Contest Posted');
})
.catch(function(error) {
_this.state = 'start';
_this.currentProject.markAsReady(false);
// Clear any previous errors
_this.error = {};
_this.error.code = error.code;
if (error.code === 'CONTEST_NAME_EXISTS') {
Analytics.trackAction('contest', 'post', error.code);
_this.showPaymentReleaseModal = true;
_this.repostContest = true;
_this.error.duplicateContest = contest.title;
} else if (error.code === 'CONTEST_CREATE_INSUFFICIENT_FUNDS') {
Analytics.trackAction('contest', 'post', error.code);
_this.state = 'success';
_this.showPaymentReleaseModal = false;
_this.draftProjectPosted = true;
} else {
Analytics.trackError('project', 'post', error.code, error.data);
_this.error.internalError = error.code;
}
});
} else {
$location.url('/signup?role=employer&return=' +
encodeURIComponent($location.url()));
return $q.reject();
}
};
// TODO: extract to service as it is used in multiple places throughout the
// app.
_this.minimumBudget = function(min) {
var minBudget = min || 10;
if(_this.project.currency) {
if(_this.project.currency.code !== 'USD') {
return Math.ceil(minBudget /
_this.project.currency.exchange_rate);
}
return minBudget;
}
if(angular.isDefined(_this.loggedInUser) &&
_this.loggedInUser.get().primary_currency.code !== 'USD') {
return Math.ceil(minBudget /
_this.loggedInUser.get().primary_currency.exchange_rate);
}
return minBudget;
};
_this.project.customBudget = _this.minimumBudget(200);
// Post a new project
_this.create = function(newProject) {
_this.error = {};
if (newProject.type === 'contest') {
delete newProject.location;
newProject.budget = newProject.budget || newProject.customBudget;
// Save the project
Projects.setCurrent(_this.currentProject);
_this.currentProject.set(newProject);
// And mark it as ready
_this.currentProject.markAsReady(true);
if (Auth.isAuthenticated()) {
var userBalance =
_this.loggedInUser.getBalanceForCurrency(newProject.currency.id);
return Deposits.getVerifiedPaymentSources()
.then(function(response) {
// Show Fund Contest modal if user can cover contest prize
if (response.payment_source.length > 0 ||
userBalance >= newProject.budget) {
// go in 'start' state to make sure there is content when user
// closes the payment release modal, if applicable
_this.state = 'start';
_this.showPaymentReleaseModal = true;
} else {
var currentUrl = encodeURIComponent($location.url());
$location.url('/deposit' +
'?postContest' +
'&amount=' + newProject.budget +
'¤cy=' + newProject.currency.id +
'&return=' + currentUrl +
'&confirm=' + encodeURIComponent('&return=' + currentUrl));
return $q.reject();
}
});
} else {
$location.url('/signup?role=employer&return=' +
encodeURIComponent($location.url()));
}
return $q.reject();
} else {
// Only get the location if it is actually local
if (newProject.local) {
// Turn google maps place result into lat/lng struct
var location = ProjectLocation.denormaliseLatLng(
_this.project.place
);
newProject.location = {
country: {
name: location.country
},
vicinity: location.locality,
latitude: location.latitude,
longitude: location.longitude,
administrative_area: location.administrative_area_level_1,
full_address: location.formatted_address
};
} else {
delete newProject.location;
}
if (newProject.type === 'hourly') {
newProject.hourly_project_info = {
duration_enum: _this.defaultProjectConfig.hourlyDuration,
commitment: {
hours: _this.defaultProjectConfig.hourlyCommitment,
interval: _this.defaultProjectConfig.hourlyInterval
}
};
}
// Save the project
Projects.setCurrent(_this.currentProject);
_this.currentProject.set(newProject);
// And mark it as ready
_this.currentProject.markAsReady(true);
// Post the project
return postProject(_this.currentProject);
}
};
// If a project is waiting to be posted, e.g. we're
// coming from signup or login, post it
if (_this.currentProject.isReady() && Auth.isAuthenticated()) {
_this.state = 'loading';
if(_this.currentProject.get().type === 'contest') {
_this.create(_this.currentProject.get());
}
else {
// Clean up postFromLoggedOut when SetTitleFromAnswer A/B test is done
postProject(_this.currentProject, true);
}
} else {
// Othewise show the project details view
_this.state = 'start';
}
// Reset the post project funnel by reloading the route
_this.reset = function() {
_this.currentProject.reset();
$location.url('/post-project');
};
// Go to the project page
_this.checkBids = function(projectId) {
$location.path('/project/' + projectId);
};
var getQuestionTemplateTranslation = function(template) {
var lang = CookieStore.get(LANGUAGE_COOKIE);
if (lang !== 'en') {
Translations.getTranslationFromString(
template.project_template_question_text.question_text,
TEMPLATEQS_TRANSLATION_DOMAIN, lang).then(
function(text) {
template.project_template_question_text.question_text = text;
});
angular.forEach(template.answers, function(a) {
Translations.getTranslationFromString(
a.answer, TEMPLATEANS_TRANSLATION_DOMAIN, lang).then(
function(text) {
if (a.answer_text.length > 0) {
a.answer = a.answer_text = text;
}
else {
a.answer = text;
}
});
});
}
};
// Project templates
if ($route.current.locals.template) {
_this.showProjectOption =
$route.current.locals.template.get().description === 'Design' ?
true : false;
// If we're using a template, initialized it
var template = $route.current.locals.template;
_this.template = template.get();
// Set project title according to template selected or if set from
// url parameters
if (!_this.currentProject.get().title) {
var paramTitle = $location.search().title;
if (paramTitle) {
_this.currentProject.setTitle(paramTitle);
} else {
_this.currentProject.setTitle(_this.template.project_title);
}
}
getQuestionTemplateTranslation(_this.template.dynamic_questions[0]);
if (!template.get().questions) {
_this.template.questions = [];
}
_this.progress = {
total: _this.template.questions ?
_this.template.questions.length + 3 : 3,
current: _this.currentProject.get().answers ?
(_this.currentProject.get().budget ?
_this.currentProject.get().answers.length + 2 :
_this.currentProject.get().answers.length) : null
};
if (!_this.currentProject.get().answers) {
_this.project.answers = [];
_this.project.freeform_answers = [];
}
_this.electAnswer = function(question, answers) {
// Remove unnecessary answers and questions if an
// earlier question has been reanswered
for (var i = 0; i < _this.project.answers.length; i++) {
if (_this.project.answers[i].question_id > question.id) {
_this.project.answers.splice(i, 1);
// Decrement i to ensure we don't iterate
// off the length of the array
i--;
}
}
// Clean up when SetTitleFromAnswer A/B test is done
// Set title from the first answer in the template
if (_this.setTitleFromAnswer && _this.project.answers.length === 1 &&
_this.template.description !== 'Software development' &&
_this.project.answers[0].answer_text.length > 0) {
_this.currentProject.setTitle(_this.project.answers[0].answer);
} else if (_this.project.answers.length === 1 &&
!_this.project.answers[0].answer_text.length) {
_this.currentProject.setTitle(_this.template.project_title);
}
for (i = 0; i < _this.template.dynamic_questions.length; i++) {
if (_this.template.dynamic_questions[i].id > question.id) {
_this.template.dynamic_questions.splice(i, 1);
i--;
}
}
delete _this.project.budget;
var nextQuestion = ProjectTemplates.getNextQuestion(
_this.template,
question,
answers);
if(nextQuestion) {
getQuestionTemplateTranslation(nextQuestion);
_this.template.dynamic_questions.push(nextQuestion);
}
// Update description
_this.project.description = ProjectTemplates.createDescription(
_this.template, answers);
// Add jobs
var jobs = [];
angular.forEach(answers, function(answer) {
angular.forEach(answer.jobs, function(job) {
jobs.push(job.id);
});
});
// Limit jobs/skills to 5 as the API requires it
_this.currentProject.setJobs(jobs.slice(0, 4));
};
_this.electFreeFormAnswer = function(question, index, freeFormAnswer) {
// Format freeform answer and add to project.answers
_this.project.answers[index] = {
question_id : question.id,
answer_text : freeFormAnswer || '',
};
_this.electAnswer(question, _this.project.answers);
};
_this.optOutTemplate = function() {
$location.search('title', _this.currentProject.get().title);
$location.path('/post-project/custom');
};
} else {
if (!_this.currentProject.get().answers) {
_this.project.answers = [];
}
// If no template is define, use an empty one
_this.template = {
dynamic_questions: [],
questions: []
};
_this.progress = {
total: 2,
current : _this.currentProject.get().budget ? 1 : 0
};
}
//
// CATEGORIES & JOB BUNDLES
//
JobBundles.getCategoriesWithBundles()
.then(function(categories) {
// Add a "Other" option to the category & job bundle dropdowns
angular.forEach(categories.getList(), function(category) {
category.job_bundles.push({
name: 'Other...', // TODO: translation support
id: 'OTHER'
});
});
categories.getList().push({
name: 'Other...', // TODO: translation support
id: 'OTHER'
});
_this.categoriesWithBundles = categories;
// Store the fallbackCategory so we can preselect the correct category
// in the dropdown
var fallbackCategoryId = parseInt($location.search()['fallback-cat'], 10);
if(fallbackCategoryId) {
_this.project.category = _this.categoriesWithBundles
.getById(fallbackCategoryId).get();
}
// If we end up with a job selected but no title, set the job
// name as the default title
if (_this.currentProject.get().bundle &&
!_this.currentProject.get().title) {
_this.project.title = _this.currentProject.get().bundle.name;
}
});
// Call when a job bundle is selected Update the project title to match the
// bundle name
_this.setBundle = function(jobList, bundle) {
if (bundle && bundle.name) {
// Set the title to the bundle name
if (!_this.currentProject.get().title) {
_this.project.title = bundle.id !== 'OTHER' ? bundle.name : '';
}
// Pre-fill the skill selector with the matching jobs
_this.project.jobs = [];
angular.forEach(bundle.jobs, function(job) {
if(jobList.getById(job)) {
_this.project.jobs.push(jobList.getById(job).get());
}
});
}
};
// Call when a category is selected. Set _this.project.local = true when
// category id picked is 9 (local jobs)
_this.setLocalOption = function(category) {
if (category.id === 9) {
_this.project.local = true;
_this.getLocation();
}
else {
_this.project.local = false;
}
};
//
// JOBS
//
Categories.getListWithJobs().then(function(categories) {
var jobList = categories.getJobs();
// If there's a bundle selected but no jobs yet, fill in the skills
if (!_this.currentProject.get().jobs &&
_this.currentProject.get().bundle) {
_this.setBundle(jobList, _this.currentProject.get().bundle);
}
// Remove the selected list from the skill list
jobList.remove(_this.currentProject.get().jobs);
// Export the job list & the jobs (used by the skill selector)
_this.jobList = jobList;
_this.jobs = jobList.getList();
// Handle skill URL parameters
if($location.search().skill_category && _this.categoriesWithBundles) {
angular.forEach(_this.categoriesWithBundles.getList(),
function(category) {
if(category.id.toString() === $location.search().skill_category) {
_this.project.category = category;
// Set project type to local if category is 'Local Jobs'
if (_this.project.category.id === 9) {
_this.project.local = true;
_this.getLocation();
}
}
});
// Handle skill subcategory URL parameters
if($location.search().skill_subcategory) {
var bundle = _this.categoriesWithBundles
.getBundlesForCategory(_this.project.category.id)
.getById(parseInt($location.search().skill_subcategory)).get();
_this.project.bundle = bundle;
_this.setBundle(jobList, bundle);
}
}
});
//
// BUDGETS & CURRENCIES
//
preloads.push(Budgets.getWithCurrencies({}));
$q.all(preloads).then(function(r) {
var bundle = r[0];
var defaultCurrency = r[1];
_this.setBudgetLists(bundle.budgets.getList());
// Export the lists
_this.budgetList = bundle.budgets;
_this.currencies = bundle.currencies.getList();
// Set default currency to match logged in user's primary currency
// If no currency selected yet, select the default currency if any or
// fall back to USD as default
if (!_this.currentProject.get().currency) {
if (angular.isDefined(_this.loggedInUser)) {
_this.project.currency = _this.loggedInUser.get().primary_currency;
}
else {
_this.project.currency = defaultCurrency ||
bundle.currencies.get({ code: 'USD' }).get();
}
}
if (!_this.currentProject.get().budget) {
// TODO: this check is very hacking. This that at some point
if ($route.current.loadedTemplateUrl.indexOf('template') === -1) {
// If we're using a Project Template, we don't need to set a budget
// or a location
_this.project.budget = _this.getDefaultBudget('fixed');
if (typeof _this.project.local === 'undefined') {
_this.project.local = false;
}
}
} else if (!_this.currentProject.get().budget.id &&
_this.currentProject.get().currency &&
_this.currentProject.get().type !== 'contest') {
// If the budget set doesn't have an ID, typically when it's a
// project repost, try to match it by maximum amount
var currentMaxBudget = _this.currentProject.get().budget.maximum;
_this.project.budget = _this.getMaxBudgetByProjType(currentMaxBudget);
}
});
// Format the budget selector labels
_this.formatBudget = function(budget, currency) {
if (budget && currency) {
var label = currencyFilter(budget.minimum, currency.sign, 0);
if (budget.maximum) {
label += ' - ' + currencyFilter(budget.maximum, currency.sign, 0);
} else {
label += '+';
}
return label;
}
};
_this.setBudgetLists = function(budgetList) {
angular.forEach(budgetList, function(budget) {
_this.budgets[budget.project_type].push(budget);
});
};
_this.getDefaultBudget = function(projectType) {
var index = _this.defaultProjectConfig.budgetIndex;
if (_this.budgets && _this.project.currency) {
return _this.budgets[projectType].filter(function(budget) {
return budget.currency_id === _this.project.currency.id;
})[index];
}
};
_this.getMaxBudgetByProjType = function(max, projectType) {
projectType = projectType || _this.defaultProjectConfig.type;
if (_this.budgetList && _this.project.currency) {
return _this.budgetList.getList().filter(function(budget) {
return budget.maximum >= max &&
budget.currency_id === _this.project.currency.id &&
budget.project_type === projectType;
})[0];
}
};
_this.changeCurrency = function() {
_this.project.budget = null;
_this.progress.current = _this.template.questions.length;
_this.project.customBudget =
Math.floor(200 / _this.project.currency.exchange_rate);
};
});
;
angular.