UNPKG

gaf-mobile

Version:

GAF mobile Web site

688 lines (607 loc) 24.5 kB
'use strict'; angular.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 + '&currency=' + 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); }; });