diff --git a/docs/MakePayment.html b/docs/MakePayment.html new file mode 100644 index 0000000..5d9821c --- /dev/null +++ b/docs/MakePayment.html @@ -0,0 +1,170 @@ + + + + + JSDoc: Class: MakePayment + + + + + + + + + + +
+ +

Class: MakePayment

+ + + + + + +
+ +
+ +

MakePayment()

+ +

Make Payment

+ + +
+ +
+
+ + + + +

Constructor

+ + + +

new MakePayment()

+ + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/Metrics.html b/docs/Metrics.html new file mode 100644 index 0000000..912ea6d --- /dev/null +++ b/docs/Metrics.html @@ -0,0 +1,222 @@ + + + + + JSDoc: Class: Metrics + + + + + + + + + + +
+ +

Class: Metrics

+ + + + + + +
+ +
+ +

Metrics()

+ + +
+ +
+
+ + + + + + +

new Metrics()

+ + + + + + +
+

Creates the view for Metrics page, which renders 4 tabs with different components being called inside each one.

+
+ + + + + + + + + + + + + +
+ + + + +
Since:
+
  • 09.28.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:Punctuality
  • + +
  • module:Ratings
  • + +
  • module:GeneralStats
  • + +
  • module:Queue
  • + +
  • module:store
  • + +
  • module:search
  • + +
  • module:Session
  • +
+ + + + + + + + + + + + + + + + + + + +
+ + + + +

Requires

+ +
    +
  • module:Punctuality
  • + +
  • module:Ratings
  • + +
  • module:GeneralStats
  • + +
  • module:Queue
  • + +
  • module:store
  • + +
  • module:search
  • + +
  • module:Session
  • +
+ + + +

Classes

+ +
+
Metrics
+
+
+ + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/actions.js.html b/docs/actions.js.html new file mode 100644 index 0000000..28b2a01 --- /dev/null +++ b/docs/actions.js.html @@ -0,0 +1,1407 @@ + + + + + JSDoc: Source: actions.js + + + + + + + + + + +
+ +

Source: actions.js

+ + + + + + +
+
+
import React from "react";
+import { useHistory } from 'react-router-dom';
+import Flux from "@4geeksacademy/react-flux-dash";
+import { Session } from "bc-react-session";
+import { Notify } from "bc-react-notifier";
+import { Shift } from "./views/shifts.js";
+import { Talent } from "./views/talents.js";
+import { Rating } from "./views/ratings.js";
+import { useNavigate } from "react-router-dom";
+import { Invite } from "./views/invites.js";
+import { Clockin, PayrollPeriod } from "./views/payroll.js";
+import moment from "moment";
+import { POST, GET, PUT, DELETE, PUTFiles, POSTcsrf, POSTcsrf2 } from "./utils/api_wrapper";
+import log from "./utils/log";
+import WEngine from "./utils/write_engine.js";
+import qs from "query-string";
+import { normalizeToSnakeCase } from "./utils/validation";
+
+const Models = {
+  shifts: Shift,
+  ratings: Rating,
+  "payroll-periods": PayrollPeriod,
+  talents: Talent,
+  employees: Talent,
+};
+
+export const autoLogin = (token = "") => {
+  Session.destroy();
+
+  return new Promise((resolve, reject) =>
+    GET("profiles/me", null, { Authorization: "JWT " + token })
+      .then(function (profile) {
+        if (!profile.employer) {
+          Notify.error(
+            "Only employers are allowed to login into this application"
+          );
+          reject("Only employers are allowed to login into this application");
+        } else if (!profile.status === "SUSPENDED") {
+          Notify.error(
+            "Your account seems to be innactive, contact support for any further details"
+          );
+          reject(
+            "Your account seems to be innactive, contact support for any further details"
+          );
+        } else {
+          const payload = {
+            user: { ...profile.user, profile },
+            access_token: token,
+          };
+          Session.start({ payload });
+          resolve(payload);
+        }
+      })
+      .catch(function (error) {
+        reject(error.message || error);
+        Notify.error(error.message || error);
+        log.error(error);
+      })
+  );
+};
+export const stripeStatus = (email, password, keep, history, id) => {
+  GET('subscription_auth/' + email).then(
+    function (value) {
+      Notify.success("Welcome!")
+
+    },
+    function (reason) {
+      history.push("/subscribe")
+      Notify.error("Your subscription is not active, please get a new one")
+    }
+  )
+
+}
+export const login = (email, password, keep, history, id) => {
+  new Promise((resolve, reject) =>
+    POST("login", {
+      username_or_email: email,
+      password: password,
+      // employer_id: Number(id),
+      exp_days: keep ? 30 : 1,
+    })
+      .then(
+        setTimeout(() => { stripeStatus(email, password, keep, history, id) }, 1000)
+      )
+      .then(function (data) {
+        // if (Number(data.user.profile.employer) != Number(id)) {
+        //     let company = data.user.profile.other_employers.find(emp => emp.employer == Number(id) );
+        //     updateCompanyUser({id: company.profile_id, employer: company.employer, employer_role: company.employer_role}, { 'Authorization': 'JWT ' + data.token });
+        //     Session.start({ {payload: {user: data.user, access_token: data.token} });
+        //     history.push('/');
+        //     resolve();
+        // }
+        if (!data.user.profile.employer) {
+          Notify.error(
+            "Only employers are allowed to login into this application"
+          );
+          reject("Only employers are allowed to login into this application");
+        } else if (!data.user.profile.status === "SUSPENDED") {
+          Notify.error(
+            "Your account seems to be innactive, contact support for any further details"
+          );
+          reject(
+            "Your account seems to be innactive, contact support for any further details"
+          );
+        } else {
+          Session.start({
+            payload: {
+              user: data.user,
+              access_token: data.token,
+            },
+          });
+          if (!data.user.profile.employer.active_subscription)
+
+            history.push("/subscribe");
+          else history.push("/");
+          resolve();
+        }
+      })
+      .catch(function (error) {
+        reject(error.message || error);
+        Notify.error(error.message || error);
+        log.error(error);
+
+      })
+  );
+}
+
+
+export const signup = (formData, history) =>
+  new Promise((resolve, reject) => {
+    POST("user/register", {
+      email: formData.email,
+      account_type: formData.account_type,
+      employer_role: formData.employer_role || "",
+      employer: formData.company || formData.employer,
+      token: formData.token || "",
+      username: formData.email,
+      first_name: formData.first_name,
+      last_name: formData.last_name,
+      password: formData.password,
+      business_name: formData.business_name,
+      business_website: formData.business_website,
+      about_business: formData.about_business,
+      phone: formData.phone,
+    })
+      .then(function (data) {
+        Notify.success("You have signed up successfully! You are being redirected to the login screen");
+        setTimeout(() => { history.push(`/login?type=${formData.account_type}`) }, 2500)
+        resolve();
+      })
+      .catch(function (error) {
+        reject(error.message || error);
+        Notify.error(error.message || error);
+        log.error(error);
+      })
+  });
+
+export const remind = (email) =>
+  new Promise((resolve, reject) =>
+    POST("user/password/reset", {
+      email: email,
+    })
+      .then(function (data) {
+        resolve();
+        Notify.success("Check your email!");
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        reject(error.message || error);
+        log.error(error);
+      })
+  );
+export const resetPassword = (formData, history) =>
+  new Promise((resolve, reject) =>
+    PUT("user/password/reset", {
+      new_password: formData.new_password,
+      repeat_password: formData.new_password,
+      token: formData.token,
+    })
+      .then(function (data) {
+        resolve();
+        Notify.success(
+          "You have change password successfully, proceed to log in"
+        );
+        history.push(`/login`);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        reject(error.message || error);
+        log.error(error);
+      })
+  );
+
+export const resendValidationLinkCurrent = (email, employer) =>
+  new Promise((resolve, reject) =>
+    POST("user/email/validate/send/" + email, {
+      email: email,
+    })
+      .then(function (data) {
+        resolve();
+        Notify.success("We have sent you a validation link, check your email!");
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        reject(error.message || error);
+        log.error(error);
+      })
+  );
+export const resendValidationLink = (email, employer) =>
+  new Promise((resolve, reject) =>
+    POST("user/email/validate/send/" + email + "/" + employer, {
+      email: email,
+      employer: employer,
+    })
+      .then(function (data) {
+        resolve();
+        Notify.success("We have sent you a validation link, check your email!");
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        reject(error.message || error);
+        log.error(error);
+      })
+  );
+
+//Send company inviation to user
+export const sendCompanyInvitation = (email, employer, employer_role, sender) =>
+  new Promise((resolve, reject) =>
+    POST(
+      "user/email/company/send/" +
+      email +
+      "/" +
+      sender +
+      "/" +
+      employer +
+      "/" +
+      employer_role,
+      {
+        email: email,
+        sender: sender,
+        employer: employer,
+        employer_role: employer_role,
+      }
+    )
+      .then(function (data) {
+        //fisrt check if I have any of this on the store
+        let entities = store.getState("jobcore-invites");
+        if (!entities || !Array.isArray(entities)) entities = [];
+
+        //if the response from the server is not a list
+        if (!Array.isArray(data)) {
+          // if the response is not a list, I will add the new object into that list
+          Flux.dispatchEvent(
+            "jobcore-invites",
+            entities.concat([{ ...data, id: data.id }])
+          );
+        }
+        //if it is an array
+        else {
+          const newShifts = data.map((inc) =>
+            Object.assign({ ...data, id: inc.id })
+          );
+          Flux.dispatchEvent("jobcore-invites", entities.concat(newShifts));
+        }
+        resolve();
+        Notify.success("We have sent the company invitation!");
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        reject(error.message || error);
+        log.error(error);
+      })
+  );
+
+export const logout = () => {
+  setTimeout(() => {
+    Session.destroy();
+    store = new _Store();
+  }, 3000);
+};
+
+/**
+ * GENERIC ACTIONS, try to reuse them!!!!
+ */
+
+export const fetchAllIfNull = (entities) => {
+  const _entities = entities.filter((e) => !store.getState("entity"));
+  return fetchAll(_entities);
+};
+export const fetchAll = (entities) =>
+  new Promise((resolve, reject) => {
+    let requests = [];
+    const checkPromiseResolution = () => {
+      const hasPending = requests.find((r) => r.pending == true);
+      if (!hasPending) {
+        const hasError = requests.find((r) => r.error == true);
+        if (hasError) reject();
+        else resolve();
+      }
+    };
+
+    entities.forEach((entity) => {
+      const currentRequest = {
+        entity: entity.slug || entity,
+        pending: true,
+        error: false,
+      };
+      requests.push(currentRequest);
+
+      GET(entity.url || entity.slug || entity)
+        .then(function (list) {
+          if (typeof entity.callback == "function") entity.callback();
+          Flux.dispatchEvent(entity.slug || entity, list);
+
+          currentRequest.pending = false;
+          checkPromiseResolution();
+        })
+        .catch(function (error) {
+          Notify.error(error.message || error);
+          log.error(error);
+
+          currentRequest.pending = false;
+          currentRequest.error = true;
+          checkPromiseResolution();
+        });
+    });
+  });
+
+export const fetchAllMeIfNull = (entities) => {
+  const _entities = entities.filter((e) => !store.getState("entity"));
+  return fetchAllMe(_entities);
+};
+export const fetchAllMe = (entities) =>
+  new Promise((resolve, reject) => {
+    let requests = [];
+    const checkPromiseResolution = () => {
+      const hasPending = requests.find((r) => r.pending == true);
+      if (!hasPending) {
+        const hasError = requests.find((r) => r.error == true);
+        if (hasError) reject();
+        else resolve();
+      }
+    };
+
+    entities.forEach((entity) => {
+      const currentRequest = {
+        entity: entity.slug || entity,
+        pending: true,
+        error: false,
+      };
+      requests.push(currentRequest);
+
+      GET("employers/me/" + (entity.slug || entity))
+        .then(function (list) {
+          if (typeof entity.callback == "function") entity.callback();
+          Flux.dispatchEvent(entity.slug || entity, list);
+
+          currentRequest.pending = false;
+          checkPromiseResolution();
+        })
+        .catch(function (error) {
+          Notify.error(error.message || error);
+          log.error(error);
+
+          currentRequest.pending = false;
+          currentRequest.error = true;
+          checkPromiseResolution();
+        });
+    });
+  });
+
+export const fetchSingle = (entity, id) =>
+  new Promise((resolve, reject) => {
+    const _entity = entity.slug || entity;
+    GET(entity.url || "employers/me/" + _entity + "/" + id)
+      .then(function (data) {
+        const cachedEntity = WEngine.get(_entity, id);
+        if (cachedEntity) data = Object.assign(data, cachedEntity);
+        Flux.dispatchEvent(
+          _entity,
+          store.replaceMerged(
+            _entity,
+            data.id,
+            Models[_entity](data).defaults().unserialize()
+          )
+        );
+        resolve(data);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject();
+      });
+  });
+
+export const processPendingPayrollPeriods = () =>
+  new Promise((resolve, reject) => {
+    const payload = Session.getPayload();
+    const params = {
+      employer:
+        payload.user.profile.employer.id || payload.user.profile.employer,
+    };
+    GET(`hook/generate_periods?${qs.stringify(params)}`)
+      .then(function (_newPeriods) {
+        let periods = store.getState("payroll-periods");
+        Flux.dispatchEvent("payroll-periods", periods.concat(_newPeriods));
+        resolve(_newPeriods);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject();
+      });
+  });
+
+export const hook = (hookName) =>
+  new Promise((resolve, reject) => {
+    const payload = Session.getPayload();
+    const params = {
+      employer:
+        payload.user.profile.employer.id || payload.user.profile.employer,
+    };
+    GET(`hook/${hookName}?${qs.stringify(params)}`)
+      .then(function (data) {
+        resolve(data);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject();
+      });
+  });
+
+export const fetchTemporal = async (url, event_name, callback = null) => {
+  try {
+    const data = await GET(url);
+    if (typeof callback == "function") callback();
+    Flux.dispatchEvent(event_name, data);
+    return data;
+  } catch (error) {
+    Notify.error(error.message || error);
+    log.error(error);
+    throw error;
+  }
+};
+
+export const search = (entity, queryString = null) =>
+  new Promise((accept, reject) =>
+    GET(entity, queryString)
+      .then(function (list) {
+        //console.log("list", list);
+        if (typeof entity.callback == "function") entity.callback();
+        Flux.dispatchEvent(entity.slug || entity, list);
+        accept(list);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      })
+  );
+export const searchMe = (entity, queryString, mergeResults = false) =>
+  new Promise((accept, reject) =>
+    GET("employers/me/" + entity, queryString)
+      .then(function (list) {
+        if (typeof entity.callback == "function") entity.callback();
+        if (mergeResults) {
+          const previous = store.getState(entity.slug || entity);
+          if (Array.isArray(previous))
+            list = previous.concat(list.results || list);
+        }
+        Flux.dispatchEvent(entity.slug || entity, list);
+        accept(list);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      })
+  );
+
+export const create = (entity, data, status = WEngine.modes.LIVE) =>
+  new Promise((resolve, reject) => {
+    POST("employers/me/" + (entity.url || entity), data)
+      .then(function (incoming) {
+        //console.log("incoming", incoming);
+        if (
+          typeof entity.url === "string" &&
+          typeof entity.slug === "undefined"
+        )
+          throw Error("Missing entity slug on the create method");
+
+        //fisrt check if I have any of this on the store
+        let entities = store.getState(entity.slug || entity);
+        if (!entities || !Array.isArray(entities)) entities = [];
+
+        //if the response from the server is not a list
+        if (!Array.isArray(incoming)) {
+          // if the response is not a list, I will add the new object into that list
+          Flux.dispatchEvent(
+            entity.slug || entity,
+            entities.concat([{ ...data, id: incoming.id }])
+          );
+        }
+        //if it is an array
+        else {
+          var newShifts;
+          if (entity === "shifts" && incoming.length > 1) {
+            newShifts = incoming;
+          } else {
+            newShifts = incoming.map((inc) =>
+              Object.assign({ ...data, id: inc.id })
+            );
+          }
+          //console.log("slug", entity.slug);
+          //console.log("entity", entity);
+          //console.log("entities", entities);
+          //console.log("newShifts", newShifts);
+          Flux.dispatchEvent(entity.slug || entity, entities.concat(newShifts));
+        }
+        Notify.success(
+          "The " +
+          (entity.slug || entity).substring(
+            0,
+            (entity.slug || entity).length - 1
+          ) +
+          " was created successfully"
+        );
+        resolve(incoming);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+export const update = (entity, data, mode = WEngine.modes.LIVE) =>
+  new Promise((resolve, reject) => {
+    let path =
+      typeof entity == "string"
+        ? `employers/me/${entity}/${data.id}`
+        : entity.path + (typeof data.id !== "undefined" ? `/${data.id}` : "");
+    const event_name = typeof entity == "string" ? entity : entity.event_name;
+    if (mode === WEngine.modes.POSPONED) path += "?posponed=true";
+    PUT(path, data)
+      .then(function (incomingObject) {
+        if (mode === WEngine.modes.POSPONED) {
+          if (event_name === "shifts")
+            data = Shift(incomingObject).defaults().unserialize();
+          WEngine.add({ entity: event_name, method: "PUT", data, id: data.id });
+        } else if (entity == "payrates") data = incomingObject;
+        else if (event_name === "current_employer")
+          Notify.success(
+            "The " + "payroll settings" + " was updated successfully"
+          );
+        else Notify.success("The " + event_name + " was updated successfully");
+        let entities = store.replaceMerged(event_name, data.id, data);
+        Flux.dispatchEvent(event_name, entities);
+        resolve(data);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+export const remove = (entity, data) => {
+  const path =
+    typeof entity == "string"
+      ? `employers/me/${entity}/${data.id}`
+      : `${entity.path}/${data.id}`;
+  const event_name = typeof entity == "string" ? entity : entity.event_name;
+  DELETE(path)
+    .then(function (incomingObject) {
+      let entities = store.remove(event_name, data.id);
+      Flux.dispatchEvent(event_name, entities);
+
+      const name = path.split("/");
+      Notify.success(
+        "The " +
+        name[0].substring(0, name[0].length - 1) +
+        " was deleted successfully"
+      );
+    })
+    .catch(function (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+};
+
+/**
+ * From here on the actions are not generic anymore
+ */
+
+export const updateProfileImage = (file) =>
+  PUTFiles("employers/me/image", [file])
+    .then(function (incomingObject) {
+      const payload = Session.getPayload();
+
+      const user = Object.assign(payload.user, { profile: incomingObject });
+      // Session.setPayload({ user });
+      return user.profile.picture;
+    })
+    .catch(function (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+
+export const updateProfile = (data) => {
+  PUT(`profiles/${data.id}`, data)
+    .then(function (incomingObject) {
+      const payload = Session.getPayload();
+      const user = Object.assign(payload.user, { profile: incomingObject });
+      Session.setPayload({ user });
+    })
+    .catch(function (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+};
+export const updateProfileMe = (data) => {
+  PUT(`profiles/me`, data)
+    .then(function (incomingObject) {
+      const payload = Session.getPayload();
+      const user = Object.assign(payload.user, { profile: incomingObject });
+      Session.setPayload({ user });
+    })
+    .catch(function (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+};
+
+export const updateEmployability = (data) => {
+  PUT(`employee/employability_expired_at/update/${data.catalog.employee.id}`,
+    data)
+    .then()
+}
+export const updateDocs = (data) => {
+  PUT(`employee/employment_verification_status/update/${data.catalog.employee.id}`,
+    data)
+    .then(response => response.json())
+    .then(data => console.log(data))
+}
+export const createSubscription = (data, history) => {
+  const employer = store.getState("current_employer");
+
+  POST(`employers/me/subscription`, data)
+    .then(function (active_subscription) {
+
+      Flux.dispatchEvent("current_employer", {
+        ...employer,
+        active_subscription,
+      });
+      Notify.success("The subscription was created successfully");
+
+
+    }).then(
+      setTimeout(() => { history.push("/welcome") }, 4000)
+
+    )
+    .catch(function (error) {
+      console.log("ERROR", error);
+      Notify.error(error.message || error);
+      log.error(error);
+    })
+
+};
+
+export const createStripePayment2 = async () => {
+  const response = await POSTcsrf2('create-payment-single-emp')
+    .then(
+      Notify.success("The payment was received successfully")
+    )
+    .catch(function (error) {
+      console.log("ERROR", error);
+      Notify.error(error.message || error);
+      log.error(error);
+    })
+
+  return response
+
+};
+
+export const createStripePayment = async (stripeToken) => {
+  const response = await POSTcsrf('create-payment-intent', stripeToken)
+    .then(
+      Notify.success("The payment was received successfully")
+    )
+    .catch(function (error) {
+      console.log("ERROR", error);
+      Notify.error(error.message || error);
+      log.error(error);
+    })
+
+  return response
+
+};
+
+export const updateSubscription = (data, history) => {
+  const employer = store.getState("current_employer");
+  PUT(`employers/me/subscription`, data)
+    .then(function (active_subscription) {
+      Flux.dispatchEvent("current_employer", {
+        ...employer,
+        active_subscription,
+      });
+      Notify.success("The subscription was updated successfully");
+    })
+    .catch(function (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+};
+
+export const removeBankAccount = (route, data) => {
+  const path = `${route}/${data.id}`;
+  DELETE(path)
+    .then(() => {
+      Notify.success("The " + data.name + " was deleted successfully");
+      searchBankAccounts();
+    })
+    .catch((error) => {
+      console.log("bank-accounts error: ", error);
+      Notify.error(error.message || error);
+      log.error(error);
+    });
+};
+
+export const rejectCandidate = async (shiftId, applicant) => {
+  let shift = store.get("shifts", shiftId);
+  if (!shift) shift = await fetchSingle("shifts", shiftId);
+  if (shift) {
+    const newCandidates = shift.candidates.filter(
+      (candidate) => candidate.id != applicant.id
+    );
+    const updatedShift = {
+      candidates: newCandidates.map((cand) => cand.id),
+    };
+
+    try {
+      await PUT(`employers/me/shifts/${shiftId}/candidates`, updatedShift);
+
+      Flux.dispatchEvent(
+        "shifts",
+        store.replaceMerged("shifts", shiftId, {
+          candidates: newCandidates,
+        })
+      );
+
+      const applications = store.getState("applications");
+      if (applications)
+        Flux.dispatchEvent(
+          "applications",
+          store.filter(
+            "applications",
+            (item) =>
+              item.shift.id != shiftId || item.employee.id != applicant.id
+          )
+        );
+
+      Notify.success("The candidate was successfully rejected");
+      return { ...shift, candidates: newCandidates };
+    } catch (error) {
+      Notify.error(error.message || error);
+      log.error(error);
+      throw error;
+    }
+  } else {
+    Notify.error("Shift not found");
+    throw new Error("Shift not found");
+  }
+};
+
+export const updateCompanyUser = (user, header = {}) =>
+  new Promise((resolve, reject) => {
+    PUT(`employers/me/users/${user.id}`, user, header)
+      .then((resp) => {
+        const users = store.getState("users");
+
+        if (users) {
+          let _users = users.map((u) => {
+            if (u.email == resp.email) return resp;
+            else return u;
+          });
+
+          Flux.dispatchEvent("users", _users);
+        }
+
+        resolve(resp);
+      })
+      .catch((error) => {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+export const updateUser = (user, header = {}) =>
+  new Promise((resolve, reject) => {
+    PUT(`employers/me/users/${user.id}`, user, header)
+      .then((resp) => {
+        const users = store.getState("users");
+
+        if (users) {
+          let _users = users.map((u) => {
+            if (u.email == resp.email) return resp;
+            else return u;
+          });
+
+          Flux.dispatchEvent("users", _users);
+        }
+
+        Notify.success("The user was successfully updated");
+        resolve(resp);
+      })
+      .catch((error) => {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+export const removeUser = (user) =>
+  new Promise((resolve, reject) => {
+    DELETE(`employers/me/users/${user.profile.id}`)
+      .then((resp) => {
+        Flux.dispatchEvent(
+          "users",
+          store.getState("users").filter((u) => u.email != user.email)
+        );
+
+        Notify.success("The user was successfully updated");
+        resolve(resp);
+      })
+      .catch((error) => {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+export const deleteShiftEmployee = async (shiftId, employee) => {
+  let shift = store.get("shifts", shiftId);
+  if (!shift) shift = await fetchSingle("shifts", shiftId);
+  if (shift) {
+    const newEmployees = shift.employees.filter((emp) => emp.id != employee.id);
+    const updatedShift = {
+      employees: newEmployees.map((emp) => emp.id),
+    };
+    PUT(`employers/me/shifts/${shiftId}/employees`, updatedShift)
+      .then(() => {
+        Flux.dispatchEvent(
+          "shifts",
+          store.replaceMerged("shifts", shiftId, {
+            employees: newEmployees,
+          })
+        );
+
+        Notify.success("The employee was successfully deleted");
+      })
+      .catch((error) => {
+        Notify.error(error.message || error);
+        log.error(error);
+      });
+  } else Notify.error("Shift not found");
+};
+
+export const acceptCandidate = async (shiftId, applicant) => {
+  let shift = store.get("shifts", shiftId);
+  if (!shift) shift = await fetchSingle("shifts", shiftId);
+  if (shift) {
+    if (
+      shift.status === "OPEN" ||
+      shift.employees.length < shift.maximum_allowed_employees
+    ) {
+      const newEmployees = shift.employees.concat([applicant]);
+      const newCandidates = shift.candidates.filter((c) =>
+        Number.isInteger(c) ? c !== applicant.id : c.id !== applicant.id
+      );
+      const shiftData = {
+        employees: newEmployees.map((emp) =>
+          Number.isInteger(emp) ? emp : emp.id
+        ),
+        candidates: newCandidates.map((can) =>
+          Number.isInteger(can) ? can : can.id
+        ),
+      };
+
+      try {
+        const data = await PUT(
+          `employers/me/shifts/${shiftId}/candidates`,
+          shiftData
+        );
+
+        const applications = store.getState("applications");
+        if (applications)
+          Flux.dispatchEvent(
+            "applications",
+            store.filter(
+              "applications",
+              (item) =>
+                item.shift.id != shiftId || item.employee.id != applicant.id
+            )
+          );
+        Flux.dispatchEvent(
+          "shifts",
+          store.replaceMerged("shifts", shiftId, {
+            employees: newEmployees,
+            candidates: newCandidates,
+          })
+        );
+        Notify.success("The candidate was successfully accepted");
+        return null;
+      } catch (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        throw error;
+      }
+    } else {
+      Notify.error("This shift is already filled.");
+      throw new Error("This shift is already filled.");
+    }
+  } else {
+    Notify.error("Shift not found");
+    throw new Error("Shift not found");
+  }
+};
+
+export const updateTalentList = (action, employee, listId) => {
+  const favoriteList = store.get("favlists", listId);
+
+  return new Promise((resolve, reject) => {
+    if (favoriteList) {
+      let employeeIdsArr = favoriteList.employees.map(
+        (employee) => employee.id || employee
+      );
+      if (action === "add") {
+        employeeIdsArr.push(employee.id || employee);
+      } else if (action === "delete") {
+        employeeIdsArr = employeeIdsArr.filter(
+          (id) => id != (employee.id || employee)
+        );
+      }
+      PUT("employers/me/favlists/" + listId, { employees: employeeIdsArr })
+        .then((updatedFavlist) => {
+          Flux.dispatchEvent(
+            "favlists",
+            store.replaceMerged("favlists", listId, {
+              employees: updatedFavlist.employees,
+            })
+          );
+          Notify.success(
+            `The talent was successfully ${action == "add" ? "added" : "removed"
+            }`
+          );
+          resolve(updatedFavlist);
+        })
+        .catch((error) => {
+          Notify.error(error.message || error);
+          log.error(error);
+          reject(error);
+        });
+    } else {
+      Notify.error("Favorite list not found");
+      reject();
+    }
+  });
+};
+
+export const updatePayments = async (payments, period) => {
+  if (!Array.isArray(payments)) payments = [payments];
+  for (let i = 0; i < payments.length; i++) {
+    let data = { ...payments[i] };
+    if (data.shift) data.shift = data.shift.id || data.shift;
+    if (data.employer) data.employer = data.employer.id || data.employer;
+    if (data.employee) data.employee = data.employee.id || data.employee;
+    if (data.clockin) data.clockin = data.clockin.id || data.clockin;
+
+    const _updated = await update("payment", data);
+    period = {
+      ...period,
+      payments: period.payments.map((p) => {
+        if (p.id === _updated.id) return { ...p, ...payments[i] };
+        else return p;
+      }),
+    };
+  }
+
+  Flux.dispatchEvent(
+    "payroll-periods",
+    store.replace("payroll-periods", period.id, period)
+  );
+  return period;
+};
+
+export const createPayment = async (payment, period) => {
+  const _new = await create("payment", {
+    ...payment,
+    employee: payment.employee.id || payment.employee,
+    shift: payment.shift.id || payment.shift,
+  });
+  const _period = {
+    ...period,
+    payments: period.payments.concat([
+      { ..._new, employee: payment.employee, shift: payment.shift },
+    ]),
+  };
+
+  Flux.dispatchEvent(
+    "payroll-periods",
+    store.replace("payroll-periods", period.id, _period)
+  );
+  return period;
+};
+
+/**
+ * Make employee payment
+ * @param  {string}  employeePaymentId employee payment id
+ * @param  {string}  paymentType payment type could be: CHECK, FAKE or ELECTRONIC TRANSFERENCE
+ * @param  {string}  employer_bank_account_id employer bank account id
+ * @param  {string}  employee_bank_account_id employee bank account id
+ */
+export const makeEmployeePayment = (
+  employeePaymentId,
+  paymentType,
+  employer_bank_account_id,
+  employee_bank_account_id,
+  deductions_list,
+  deductions
+) =>
+  new Promise((resolve, reject) => {
+    const data = {
+      payment_type: paymentType,
+      payment_data:
+        paymentType === "CHECK"
+          ? {}
+          : {
+            employer_bank_account_id: employer_bank_account_id,
+            employee_bank_account_id: employee_bank_account_id,
+          },
+      deductions_list: deductions_list,
+      deductions: deductions,
+    };
+
+    POST(`employers/me/employee-payment/${employeePaymentId}`, data)
+      .then((resp) => {
+        Flux.dispatchEvent("employee-payment", resp);
+        Notify.success("Payment was successful");
+        resolve(resp);
+      })
+      .catch((error) => {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+/**
+ * Fetch payroll period payments
+ * @param  {string}  payrollPeriodId employee payment id
+ */
+export const fetchPeyrollPeriodPayments = async (payrollPeriodId) => {
+  try {
+    const response = await GET(
+      `employers/me/employee-payment-list/${payrollPeriodId}`
+    );
+    Flux.dispatchEvent("payroll-period-payments", response);
+  } catch (error) {
+    Notify.error(error.message || error);
+  }
+};
+
+export const addBankAccount = (token, metadata) =>
+  new Promise((resolve, reject) =>
+    POST(
+      "bank-accounts/",
+      normalizeToSnakeCase({
+        publicToken: token,
+        institutionName: metadata.institution.name,
+      })
+    )
+      .then(function (data) {
+        console.log("addBankAccount data: ", data);
+        resolve();
+        searchBankAccounts();
+      })
+      .catch(function (error) {
+        reject(error.message || error);
+        Notify.error(error.message || error);
+        log.error(error);
+      })
+  );
+
+export const searchBankAccounts = () =>
+  new Promise((accept, reject) =>
+    GET("bank-accounts/")
+      .then(function (list) {
+        console.log("bank-accounts list: ", list);
+        Flux.dispatchEvent("bank-accounts", list);
+        accept(list);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      })
+  );
+
+/**
+ * Get payments report
+ * @param  {string}  periodId payroll period id
+ * @param  {string}  startDate start date
+ * @param  {string}  endDate end date
+ */
+export const getPaymentsReport = (periodId, startDate, endDate) =>
+  new Promise((accept, reject) => {
+    const route = `employers/me/employee-payment/report?start_date=${startDate}&end_date=${endDate}&period_id=${periodId}`;
+    GET(route)
+      .then(function (list) {
+        Flux.dispatchEvent("payments-reports", list);
+        accept(list);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+/**
+ * Get deductions report
+ * @param  {string}  periodId payroll period id
+ * @param  {string}  startDate start date
+ * @param  {string}  endDate end date
+ */
+export const getDeductionsReport = (periodId, startDate, endDate) =>
+  new Promise((accept, reject) => {
+    const route = `employers/me/employee-payment/deduction-report?start_date=${startDate}&end_date=${endDate}&period_id=${periodId}`;
+    GET(route)
+      .then(function (list) {
+        Flux.dispatchEvent("deductions-reports", list);
+        accept(list);
+      })
+      .catch(function (error) {
+        Notify.error(error.message || error);
+        log.error(error);
+        reject(error);
+      });
+  });
+
+// export const createPayrollPeriodRating = (entity, queryString) => new Promise((accept, reject) =>
+//     GET('employers/me/' + entity, queryString)
+//         .then(function (list) {
+//             if (typeof entity.callback == 'function') entity.callback();
+//             Flux.dispatchEvent(entity.slug || entity, list);
+//             accept(list);
+//         })
+//         .catch(function (error) {
+//             Notify.error(error.message || error);
+//             log.error(error);
+//             reject(error);
+//         })
+// );
+
+export const http = { GET };
+
+class _Store extends Flux.DashStore {
+  constructor() {
+    super();
+    this.addEvent("positions");
+    this.addEvent("venues");
+    this.addEvent("onboarding");
+    this.addEvent("users");
+    this.addEvent("invites", (invites) => {
+      if (!Array.isArray(invites)) return [];
+      return invites.map((inv) => Invite(inv).defaults().unserialize());
+    });
+    this.addEvent("payment");
+    this.addEvent("employee-payment");
+    this.addEvent("clockins", (clockins) =>
+      !Array.isArray(clockins)
+        ? []
+        : clockins.map((c) => ({
+          ...c,
+          started_at: moment(c.starting_at),
+          ended_at: moment(c.ended_at),
+        }))
+    );
+    this.addEvent("jobcore-invites");
+    this.addEvent("ratings", (_ratings) =>
+      !Array.isArray(_ratings)
+        ? []
+        : _ratings.map((ra) => Rating(ra).defaults().unserialize())
+    );
+    this.addEvent("bank-accounts");
+    this.addEvent("employees", (employees) => {
+      if (!Array.isArray(employees)) return [];
+      return employees
+        .filter((em) => em.user.profile)
+        .map((tal) => Talent(tal).defaults().unserialize());
+    });
+    this.addEvent("favlists");
+    this.addEvent("company-user");
+    this.addEvent("deduction");
+    this.addEvent("payrates");
+    this.addEvent("payroll-period-payments");
+    this.addEvent("payments-reports");
+    this.addEvent("deductions-reports");
+    this.addEvent("badges");
+
+    this.addEvent("applications", (applicants) => {
+      return !applicants ||
+        (Object.keys(applicants).length === 0 &&
+          applicants.constructor === Object)
+        ? []
+        : applicants.map((app) => {
+          app.shift = Shift(app.shift).defaults().unserialize();
+          return app;
+        });
+    });
+    this.addEvent("shifts", (shifts) => {
+      shifts = Array.isArray(shifts.results)
+        ? shifts.results
+        : Array.isArray(shifts)
+          ? shifts
+          : null;
+      let newShifts =
+        !shifts ||
+          (Object.keys(shifts).length === 0 && shifts.constructor === Object)
+          ? []
+          : shifts
+            .filter((s) => s.status !== "CANCELLED")
+            .map((shift) => {
+              //already transformed
+              return Shift(shift).defaults().unserialize();
+            });
+
+      const applicants = this.getState("applications");
+      if (!applicants && Session.get().isValid) fetchAllMe(["applications"]);
+
+      // const _shift = newShifts.find(s => s.id == 1095);
+      return newShifts;
+    });
+
+    // Payroll related data
+    // this.addEvent('payroll-periods', (period) => {
+    //     return (!period || (Object.keys(period).length === 0 && period.constructor === Object)) ? [{ label: "Loading payment periods...", value: null }] : period.map(p => {
+    //         p.label = `From ${moment(p.starting_at).format('MM-D-YY h:mm A')} to ${moment(p.ending_at).format('MM-D-YY h:mm A')}`;
+    //         if(!Array.isArray(p.payments)) p.payments = [];
+    //         return p;
+    //     });
+    // });
+    this.addEvent("payroll-periods");
+    this.addEvent("subscription");
+    this.addEvent("w4-form");
+    this.addEvent("previos-employee-shifts");
+    this.addEvent("employee-expired-shifts"); //temporal, just used on the payroll report
+
+    //temporal storage (for temporal views, information that is read only)
+    this.addEvent("current_employer", (employer) => {
+      employer.payroll_configured =
+        employer.payroll_period_starting_time != null;
+      employer.payroll_period_starting_time = moment.isMoment(
+        employer.payroll_period_starting_time
+      )
+        ? employer.payroll_period_starting_time
+        : employer.payroll_period_starting_time
+          ? moment(employer.payroll_period_starting_time)
+          : moment(employer.created_at).startOf("isoWeek");
+      return employer;
+    });
+    this.addEvent("single_payroll_detail", (payroll) => {
+      const clockins = payroll.clockins;
+      let approved = true;
+      let paid = true;
+      payroll.clockins =
+        !clockins ||
+          (Object.keys(clockins).length === 0 && clockins.constructor === Object)
+          ? []
+          : clockins.map((clockin) => {
+            //already transformed
+            if (clockin.status == "PENDING") {
+              approved = false;
+              paid = false;
+            } else if (clockin.status != "PAID") paid = false;
+
+            return Clockin(clockin).defaults().unserialize();
+          });
+
+      if (typeof payroll.talent != "undefined")
+        payroll.talent.paymentsApproved = approved;
+      if (typeof payroll.talent != "undefined")
+        payroll.talent.paymentsPaid = paid;
+      payroll.approved = approved;
+      payroll.paid = paid;
+      return payroll;
+    });
+  }
+
+  get(type, id) {
+    const entities = this.getState(type);
+    if (entities)
+      return entities.find(
+        (ent) => ent.id == parseInt(id, 10) || ent.value == parseInt(id, 10)
+      );
+    else return null;
+  }
+  add(type, item) {
+    const entities = this.getState(type);
+    if (item) return entities.concat([item]);
+    //else return entities;
+    else throw new Error("Trying to add a null item into " + type);
+  }
+  replace(type, id, item) {
+    const entities = this.getState(type);
+    if (!entities) throw new Error("No item found in " + type);
+
+    if (Array.isArray(entities)) {
+      return entities.concat([]).map((ent) => {
+        if (ent.id != parseInt(id, 10)) return ent;
+        return item;
+      });
+    } else return item;
+  }
+  replaceMerged(type, id, item) {
+    let entities = this.getState(type);
+    if (!entities) entities = [];
+
+    if (Array.isArray(entities)) {
+      const result = entities.concat([]).map((ent) => {
+        if (ent.id != parseInt(id, 10)) return ent;
+        return Object.assign(ent, item);
+      });
+      return result;
+    } else {
+      return Object.assign(entities, item);
+    }
+  }
+  remove(type, id) {
+    const entities = this.getState(type);
+    if (entities)
+      return entities.filter((ent) => {
+        return ent.id != parseInt(id, 10);
+      });
+    else throw new Error("No items found in " + entities);
+  }
+
+  filter(type, callback) {
+    const entities = this.getState(type);
+    if (entities) return entities.filter(callback);
+    else throw new Error("No items found in entity type: " + type);
+  }
+}
+export let store = new _Store();
+
+
+
+ + + + +
+ + + +
+ + + + + + + diff --git a/docs/fonts/OpenSans-Bold-webfont.eot b/docs/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..5d20d91 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.eot differ diff --git a/docs/fonts/OpenSans-Bold-webfont.svg b/docs/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/docs/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Bold-webfont.woff b/docs/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..1205787 Binary files /dev/null and b/docs/fonts/OpenSans-Bold-webfont.woff differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.eot b/docs/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..1f639a1 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.svg b/docs/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..6a2607b --- /dev/null +++ b/docs/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-BoldItalic-webfont.woff b/docs/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..ed760c0 Binary files /dev/null and b/docs/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Italic-webfont.eot b/docs/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..0c8a0ae Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.eot differ diff --git a/docs/fonts/OpenSans-Italic-webfont.svg b/docs/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..e1075dc --- /dev/null +++ b/docs/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Italic-webfont.woff b/docs/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..ff652e6 Binary files /dev/null and b/docs/fonts/OpenSans-Italic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Light-webfont.eot b/docs/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..1486840 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.eot differ diff --git a/docs/fonts/OpenSans-Light-webfont.svg b/docs/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/docs/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Light-webfont.woff b/docs/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..e786074 Binary files /dev/null and b/docs/fonts/OpenSans-Light-webfont.woff differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.eot b/docs/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..8f44592 Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/docs/fonts/OpenSans-LightItalic-webfont.svg b/docs/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/docs/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-LightItalic-webfont.woff b/docs/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..43e8b9e Binary files /dev/null and b/docs/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/docs/fonts/OpenSans-Regular-webfont.eot b/docs/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..6bbc3cf Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.eot differ diff --git a/docs/fonts/OpenSans-Regular-webfont.svg b/docs/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..25a3952 --- /dev/null +++ b/docs/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/fonts/OpenSans-Regular-webfont.woff b/docs/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..e231183 Binary files /dev/null and b/docs/fonts/OpenSans-Regular-webfont.woff differ diff --git a/docs/global.html b/docs/global.html new file mode 100644 index 0000000..3cea2d7 --- /dev/null +++ b/docs/global.html @@ -0,0 +1,5003 @@ + + + + + JSDoc: Global + + + + + + + + + + +
+ +

Global

+ + + + + + +
+ +
+ +

+ + +
+ +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + +
+ + + + + + + + + + + + + + +

Members

+ + + +

(constant) AddFavlistsToTalent

+ + + + +
+

Add To Favorite List

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ApplicantExtendedCard

+ + + + +
+

Applican Card

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ApplicationDetails

+ + + + +
+

Application Details

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) CreateDeduction

+ + + + +
+

Create deduction

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) EditOrAddExpiredShift

+ + + + +
+

EditOrAddExpiredShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) fetchAllIfNull

+ + + + +
+

GENERIC ACTIONS, try to reuse them!!!!

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) fetchPeyrollPeriodPayments

+ + + + +
+

Fetch payroll period payments

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) FilterApplications

+ + + + +
+

Filter Applications

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) FilterLocations

+ + + + +
+

AddShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) FilterShifts

+ + + + +
+

FilterShifts

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) FilterTalents

+ + + + +
+

AddShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) GET

+ + + + +
+

Fetch JSON from API through GET method

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) getDeductionsReport

+ + + + +
+

Get deductions report

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) getPaymentsReport

+ + + + +
+

Get payments report

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) InviteTalentToJobcore

+ + + + +
+

ShiftDetails

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) InviteUserToCompanyJobcore

+ + + + +
+

Invite a new user to the company

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) makeEmployeePayment

+ + + + +
+

Make employee payment

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) PendingInvites

+ + + + +
+

ShiftDetails

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) PendingJobcoreInvites

+ + + + +
+

ShiftDetails

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) PendingRatings

+ + + + +
+

Talent Details

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) RateShift

+ + + + +
+

RateShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) RatingDetails

+ + + + +
+

Talent Details

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) RatingEmployees

+ + + + +
+

Review Talent in general

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ReviewTalentAndShift

+ + + + +
+

Revire Talent for a specific shift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) SearchShiftToInviteTalent

+ + + + +
+

AddShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) SearchTalentToInviteToShift

+ + + + +
+

Invite Talent To Shift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ShiftApplicants

+ + + + +
+

ShiftApplicants

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ShiftDetails

+ + + + +
+

ShiftDetails

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ShiftEmployees

+ + + + +
+

ShiftApplicants

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ShiftInvites

+ + + + +
+

ShiftApplicants

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) ShiftTalentClockins

+ + + + +
+

RateShift

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) TalentDetails

+ + + + +
+

Talent Details

+

Before, the Stars component was rendered inside a p tag, +now its rendered inside a span tag

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) UpdateDeduction

+ + + + +
+

Edit deduction

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + +

(constant) updateProfileImage

+ + + + +
+

From here on the actions are not generic anymore

+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + +

Methods

+ + + + + + + +

BarChart(barData)

+ + + + + + +
+

Creates a bar chart with the data passed as an argument

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
barData + + +object + + + +

Object with data like colors, labels, and values needed for the chart.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:Bar
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

ClockInsDataGenerator(props)

+ + + + + + +
+

Generates array of objects with clock-in trends for Punctuality.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

ClockOutsDataGenerator(props)

+ + + + + + +
+

Generates array of objects with clock-out trends for Punctuality.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

createMapOptions()

+ + + + + + +
+

Add a Location

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

EditOrAddShift()

+ + + + + + +
+

EditOrAddShift

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

FeatureIndicator()

+ + + + + + +
+

YourSubscription

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

filterClockins()

+ + + + + + +
+

SelectTimesheet

+
+ + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +

GeneralStats(props)

+ + + + + + +
+

Creates a page with 3 tabs that show metrics about Shifts, Punctuality, and Hours.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all shifts, and an array of all the workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:Hours
  • + +
  • module:Shifts
  • + +
  • module:JobSeekers
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Hours()

+ + + + + + +
+

Creates a page with a table and a graph of the hours worked and their trends.

+
+ + + + + + + + + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:PieChart
  • + +
  • module:HoursData
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

HoursDataGenerator(props)

+ + + + + + +
+

Takes in list a of shifts and generates data of all the hours trends for Hours.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

JobSeekers(props)

+ + + + + + +
+

Creates a page with 2 graphs and 2 charts showing trends on active, inactive, and new job seekers.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts, and also an array of all the workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:PieChart
  • + +
  • module:BarChart
  • + +
  • module:NewJobSeekersDataGenerator
  • + +
  • module:JobSeekersDataGenerator
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

JobSeekersDataGenerator(props)

+ + + + + + +
+

Takes in list a of shifts and job seekers and generates data of inactive/active job seekers for JobSeekers.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts, and also an array of all the workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

NewJobSeekersDataGenerator(props)

+ + + + + + +
+

Takes in list a of job seekers and generates data of new job seekers for JobSeekers.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts, and also an array of all the workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

PieChart(pieData)

+ + + + + + +
+

Creates a pie chart with the data passed as an argument.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
pieData + + +object + + + +

Object with data like colors, labels, and values needed for the chart.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:Pie
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Punctuality(props)

+ + + + + + +
+

Creates a page with 2 tables and 2 graphs of the clock-in and clock-out trends.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:PieChart
  • + +
  • module:ClockInsDataGenerator
  • + +
  • module:ClockOutsDataGenerator
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Queue(props)

+ + + + + + +
+

Creates a page with a DatePicker and table of all employees with their worked/scheduled hours.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all shifts, and an array of all workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • + +
  • module:DatePicker
  • + +
  • module:QueueData
  • + +
  • module:Button
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

QueueData(props)

+ + + + + + +
+

Creates a table of all employees with their worked/scheduled hours for Queue.js

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all shifts, and an object with information of a single worker, previously mapped in Queue.js

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:moment
  • + +
  • module:Avatar
  • + +
  • module:Button
  • + +
  • module:Theme
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Ratings(props)

+ + + + + + +
+

Creates a pie chart and a table reflecting how many job seekers are in each category of star ratings (1 to 5 stars.)

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all shifts, and an array of all workers.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:PieChart
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Shifts(props)

+ + + + + + +
+

Creates a page with a table and a graph of all the shift statuses.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains an array of all the shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + +
Requires:
+
    +
  • module:ShiftsDataGenerator
  • + +
  • module:BarChart
  • +
+ + + + + + + + + + + + + + + + + + + + + + + + +

ShiftsDataGenerator(props)

+ + + + + + +
+

Takes in list a of shifts and generates data of shift statuses for Shifts.js.

+
+ + + + + + + + + +
Parameters:
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameTypeDescription
props + + +object + + + +

Contains a list of all shifts.

+ + + + + + +
+ + + + +
Since:
+
  • 09.29.22 by Paola Sanchez
+ + + + + + + + + + + + + + + +
Author:
+
+
    +
  • Paola Sanchez
  • +
+
+ + + + + + + + + +
Source:
+
+ + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/index.html b/docs/index.html new file mode 100644 index 0000000..3d5cba9 --- /dev/null +++ b/docs/index.html @@ -0,0 +1,72 @@ + + + + + JSDoc: JSDoc of employer-web-client + + + + + + + + + + +
+ +

JSDoc of employer-web-client

+ + + + + + + + +

+ + + + + + + + + + + + + + + +
+

Welcome to the documentation of JobCore's employer-web-client repo.

+

Here, you can explore the documentation of each function and block of code we have.

+

To navigate this documentation, please look at the list of names on the right side of the screen, under "Global."

+

There, you will find each documentation file.

+
+ + + + + + +
+ + + +
+ + + + + + + \ No newline at end of file diff --git a/docs/scripts/linenumber.js b/docs/scripts/linenumber.js new file mode 100644 index 0000000..4354785 --- /dev/null +++ b/docs/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/docs/scripts/prettify/Apache-License-2.0.txt b/docs/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/docs/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/docs/scripts/prettify/lang-css.js b/docs/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/docs/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/docs/scripts/prettify/prettify.js b/docs/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/docs/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/docs/styles/prettify-jsdoc.css b/docs/styles/prettify-jsdoc.css new file mode 100644 index 0000000..5a2526e --- /dev/null +++ b/docs/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/docs/styles/prettify-tomorrow.css b/docs/styles/prettify-tomorrow.css new file mode 100644 index 0000000..b6f92a7 --- /dev/null +++ b/docs/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/docs/utils_api_wrapper.js.html b/docs/utils_api_wrapper.js.html new file mode 100644 index 0000000..23cacc0 --- /dev/null +++ b/docs/utils_api_wrapper.js.html @@ -0,0 +1,369 @@ + + + + + JSDoc: Source: utils/api_wrapper.js + + + + + + + + + + +
+ +

Source: utils/api_wrapper.js

+ + + + + + +
+
+
/* global localStorage, fetch */
+import { logout } from '../actions';
+import log from './log';
+import { Session } from 'bc-react-session';
+import { setLoading } from '../components/load-bar/LoadBar.jsx';
+// import { getCookie } from '../csrftoken';
+import Cookies from 'js-cookie'
+
+
+const rootAPIendpoint = process.env.API_HOST + '/api';
+
+let HEADERS = {
+  'Content-Type': 'application/json'
+};
+
+// TODO: implemente a queue for requests and status, also avoid calling the same request twice
+let PendingReq = {
+  _requests: [],
+  add: function (req) {
+    this._requests.push(req);
+    setLoading(true);
+  },
+  remove: function (req) {
+    this._requests = this._requests.filter(r => r !== req);
+    if (this._requests.length == 0) {
+      setLoading(false);
+    }
+  }
+};
+
+const getToken = () => {
+  if (Session) {
+    const payload = Session.getPayload();
+    const token = payload.access_token;
+    return token;
+  }
+  return null;
+};
+
+const appendCompany = (data) => {
+  if (Session && data) {
+    const payload = Session.getPayload();
+    data.employer = payload.user.profile.employer.id || payload.user.profile.employer;
+    return data;
+  }
+};
+
+/* AVAILABLE MODELS
+  - badges
+  - employees
+  - employers
+  - favlists
+  - positions
+  - profiles
+  - shifts
+  - venues
+  - oauth/token (generate token)
+  - tokenuser (get user data from local saved token)
+*/
+
+/**
+ * Fetch JSON from API through GET method
+ * @param {string} model Model data to be fetched. **Must be plural**
+ * @returns {data}
+ */
+export const GET = async (endpoint, queryString = null, extraHeaders = {}) => {
+  let url = `${rootAPIendpoint}/${endpoint}`;
+  if (queryString) url += queryString;
+
+  HEADERS['Authorization'] = `JWT ${getToken()}`;
+  const REQ = {
+    method: 'GET',
+    headers: Object.assign(HEADERS, extraHeaders)
+  };
+
+  const req = new Promise((resolve, reject) => fetch(url, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  PendingReq.add(req);
+  return req;
+};
+
+export const POST = (endpoint, postData, extraHeaders = {}) => {
+  if (['user/register', 'login', 'user/password/reset','employers/me/jobcore-invites'].indexOf(endpoint) == -1) {
+    HEADERS['Authorization'] = `JWT ${getToken()}`;
+    postData = appendCompany(postData);
+  }
+  
+  const REQ = {
+    method: 'POST',
+    headers: Object.assign(HEADERS, extraHeaders),
+    body: JSON.stringify(postData),
+    // mode: 'no-cors'
+  };
+
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+
+//   const req = new Promise((resolve, reject) => {
+//     const loadData = async () => {
+//       const res = await fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+//       const data = await res.json();
+//     }
+//     loadData();
+// });
+
+  PendingReq.add(req);
+  return req;
+};
+// function getCookie(name) {
+//   let cookieValue = null;
+
+//   if (document.cookie && document.cookie !== '') {
+//       const cookies = document.cookie.split(';');
+//       for (let i = 0; i < cookies.length; i++) {
+//           const cookie = cookies[i].trim();
+
+//           // Does this cookie string begin with the name we want?
+//           if (cookie.substring(0, name.length + 1) === (name + '=')) {
+//               cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
+
+//               break;
+//           }
+//       }
+//   }
+
+//   return cookieValue;
+// }
+// var csrftoken = getCookie('csrftoken');
+// var headers = new Headers();
+// headers.append('X-CSRFToken', csrftoken);
+export const POSTcsrf = (endpoint, postData, extraHeaders = {}) => {
+  // Cookies.get('csrftoken')
+  // console.log("postData###", postData)
+  Cookies.set('stripetoken', postData.id)
+  if (['user/register', 'login', 'user/password/reset','employers/me/jobcore-invites'].indexOf(endpoint) == -1) {
+    HEADERS['Authorization']  = `JWT ${getToken()}`,`X-CSRFToken ${Cookies.get('stripetoken')}`
+    postData = appendCompany(postData);
+  }
+
+  const REQ = {
+    method: 'POST',
+    headers: Object.assign(HEADERS, extraHeaders),
+    body: JSON.stringify(postData),
+    // mode: 'no-cors'
+  };
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  
+  PendingReq.add(req);
+  return req;
+};
+
+export const POSTcsrf2 = (endpoint, postData, extraHeaders = {}) => {
+  // Cookies.get('csrftoken')
+  // console.log("postData###", postData)
+  // Cookies.set('stripetoken', postData.id)
+  if (['user/register', 'login', 'user/password/reset','employers/me/jobcore-invites'].indexOf(endpoint) == -1) {
+    HEADERS['Authorization']  = `JWT ${getToken()}`,`X-CSRFToken ${Cookies.get('stripetoken')}`
+    postData = appendCompany(postData);
+  }
+
+  const REQ = {
+    method: 'POST',
+    headers: Object.assign(HEADERS, extraHeaders),
+    body: JSON.stringify(postData),
+    // mode: 'no-cors'
+  };
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  
+  PendingReq.add(req);
+  return req;
+};
+
+// fetch('/api/upload', {
+//     method: 'POST',
+//     body: payload,
+//     headers: headers,
+//     credentials: 'include'
+// }).  
+export const PUTFiles = (endpoint, files) => {
+  const headers = {
+    'Authorization': `JWT ${getToken()}`
+  };
+
+  var fetchBody = new FormData();
+  for (const file of files) fetchBody.append('image', file, file.name);
+
+  const REQ = {
+    headers,
+    method: 'PUT',
+    body: fetchBody
+  };
+
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  PendingReq.add(req);
+
+  return req;
+};
+
+export const PUT = (endpoint, putData, extraHeaders = {}) => {
+  if (['register', 'login','user/password/reset'].indexOf(endpoint) == -1) {
+    HEADERS['Authorization'] = `JWT ${getToken()}`;
+  }
+  const REQ = {
+    method: 'PUT',
+    headers: Object.assign(HEADERS, extraHeaders),
+    body: JSON.stringify(putData)
+  };
+
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  PendingReq.add(req);
+  
+  return req;
+};
+
+export const DELETE = (endpoint, extraHeaders = {}) => {
+  HEADERS['Authorization'] = `JWT ${getToken()}`;
+
+  const REQ = {
+    method: 'DELETE',
+    headers: Object.assign(HEADERS, extraHeaders)
+  };
+
+  const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ)
+    .then((resp) => processResp(resp, req))
+    .then(data => resolve(data))
+    .catch(err => {
+      processFailure(err, req);
+      reject(err);
+    })
+  );
+  PendingReq.add(req);
+  return req;
+};
+
+const processResp = function (resp, req = null) {
+  PendingReq.remove(req);
+  if (resp.ok) {
+    if (resp.status == 204) return new Promise((resolve, reject) => resolve(true));
+    else return resp.json();
+  }
+  else return new Promise(function (resolve, reject) {
+    if (resp.status == 400) parseError(resp).catch((errorMsg) => reject(errorMsg));
+    else if (resp.status == 404) reject(new Error('Not found'));
+    else if (resp.status == 503) {
+      logout();
+      reject(new Error('The JobCore API seems to be unavailable'));
+    }
+    else if (resp.status == 401) {
+      logout();
+      reject(new Error('You are not authorized for this action'));
+    }
+    else if (resp.status >= 500 && resp.status < 600) {
+      resp.json().then(err => reject(new Error(err.detail)))
+        .catch((errorMsg) => reject(new Error('Something bad happened while completing your request! Please try again later.')));
+    }
+    else reject(new Error('Something went wrong'));
+  });
+};
+
+const processFailure = function (err, req = null) {
+  PendingReq.remove(req);
+  log.error(err);
+};
+
+const parseError = (error) => new Promise(function (resolve, reject) {
+  const errorPromise = error.json();
+  errorPromise.then(json => {
+    let errorMsg = '';
+    for (let type in json) {
+      if (Array.isArray(json[type])) errorMsg += json[type].join(',');
+      else if (typeof json[type] === 'object' && json[type] !== null) errorMsg += Object.values(json[type]).join(',');
+      else errorMsg += json[type];
+    }
+    reject(errorMsg);
+  })
+    .catch(error => {
+      reject(error);
+    });
+});
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_applications.js.html b/docs/views_applications.js.html new file mode 100644 index 0000000..717c4b9 --- /dev/null +++ b/docs/views_applications.js.html @@ -0,0 +1,376 @@ + + + + + JSDoc: Source: views/applications.js + + + + + + + + + + +
+ +

Source: views/applications.js

+ + + + + + +
+
+
import React from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+import { AcceptReject, Avatar, Stars, Theme, Wizard } from '../components/index';
+import { store, rejectCandidate, acceptCandidate, fetchAllMe } from '../actions.js';
+import queryString from 'query-string';
+import { TIME_FORMAT, DATE_FORMAT } from '../components/utils.js';
+import moment from 'moment';
+import { callback, hasTutorial } from '../utils/tutorial';
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getApplicationsInitialFilters = (catalog) => {
+    let query = queryString.parse(window.location.search);
+    if (typeof query == 'undefined') return {};
+    if (!Array.isArray(query.positions)) query.positions = (typeof query.positions == 'undefined') ? [] : [query.positions];
+    if (!Array.isArray(query.venues)) query.venues = (typeof query.venues == 'undefined') ? [] : [query.venues];
+    return {
+        positions: query.positions.map(pId => catalog.positions.find(pos => pos.value == pId)),
+        venues: query.venues.map(bId => catalog.venues.find(b => b.value == bId))
+    };
+};
+
+export const Application = (data) => {
+
+    const _defaults = {
+        //foo: 'bar',
+        serialize: function () {
+
+            const newEntity = {
+                //foo: 'bar'
+                // favoritelist_set: data.favoriteLists.map(fav => fav.value)
+            };
+
+            return Object.assign(this, newEntity);
+        }
+    };
+
+    let _entity = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+
+            return _entity;
+        },
+        defaults: () => {
+            return _defaults;
+        },
+        getFormData: () => {
+            // const _formShift = {
+            //     id: _entity.id,
+            //     favoriteLists: _entity.favoriteLists.map(fav => ({ label: fav.title, value: fav.id }))
+            // };
+            // return _formShift;
+        },
+        filters: () => {
+            const _filters = {
+                positions: _entity.positions.map(item => item.value),
+                minimum_hourly_rate: _entity.minimum_hourly_rate,
+                venues: _entity.venues.map(item => item.value)
+            };
+            for (let key in _entity) if (typeof _entity[key] == 'function') delete _entity[key];
+            return Object.assign(_entity, _filters);
+        }
+    };
+};
+
+export class ManageApplicantions extends Flux.DashView {
+
+    constructor() {
+        super();
+        this.state = {
+            applicants: [],
+            runTutorial: hasTutorial(),
+            steps: [
+                {
+                    target: '#applicant_details_header',
+                    content: 'Here is everyone that has applied to your shifts but you haven\'t accepted or rejected',
+                    placement: 'right'
+                },
+                {
+                    target: '#filter_applicants',
+                    content: 'You can also filter this list of applicants by any desired criteria',
+                    placement: 'left'
+                }
+            ]
+        };
+    }
+
+    componentDidMount() {
+
+        this.filter();
+        this.subscribe(store, 'applications', (applicants) => {
+            this.filter(applicants);
+        });
+
+        this.props.history.listen(() => {
+            this.filter();
+        });
+        this.setState({ runTutorial: true });
+        fetchAllMe(['applications']);
+
+    }
+
+    filter(applicants = null) {
+        let filters = this.getFilters();
+        if (!applicants) applicants = store.getState('applications');
+        if (applicants) {
+            this.setState({
+                applicants: applicants.filter((applicant) => {
+                    for (let f in filters) {
+                        const matches = filters[f].matches(applicant);
+                        if (!matches) return false;
+                    }
+
+                    return true;
+                }).sort((applicant) => moment().diff(applicant.created_at, 'minutes'))
+            });
+        }
+        else this.setState({ applicant: [] });
+    }
+
+    getFilters() {
+        let filters = queryString.parse(window.location.search);
+        for (let f in filters) {
+            switch (f) {
+                case "positions":
+                    filters[f] = {
+                        value: filters[f],
+                        matches: (application) => {
+                            if (!filters.positions || typeof filters.positions == undefined) return true;
+                            else if (!Array.isArray(filters.positions.value)) {
+                                return filters.positions.value == application.shift.position.id;
+                            }
+                            else {
+                                if (filters.positions.value.length == 0) return true;
+                                return filters.positions.value.find(posId => application.shift.position.id == posId) !== null;
+                            }
+                        }
+                    };
+                    break;
+                case "minimum_hourly_rate":
+                    filters[f] = {
+                        value: filters[f],
+                        matches: (application) => {
+                            if (!filters.minimum_hourly_rate.value) return true;
+                            if (isNaN(filters.minimum_hourly_rate.value)) return true;
+                            return parseInt(application.shift.minimum_hourly_rate, 10) >= filters.minimum_hourly_rate.value;
+                        }
+                    };
+                    break;
+                case "venues":
+                    filters[f] = {
+                        value: filters[f],
+                        matches: (application) => {
+                            if (!filters.venues || typeof filters.venues == undefined) return true;
+                            else if (!Array.isArray(filters.venues.value)) {
+                                return filters.venues.value == application.shift.venue.id;
+                            }
+                            else {
+                                if (filters.venues.value.length == 0) return true;
+                                return filters.venues.value.find(posId => application.shift.venue.id == posId) !== null;
+                            }
+                        }
+                    };
+                    break;
+                case "date":
+                    filters[f] = {
+                        value: filters[f],
+                        matches: (shift) => {
+                            const fdate = moment(filters.date.value);
+                            return shift.date.diff(fdate, 'days') == 0;
+                        }
+                    };
+                    break;
+            }
+        }
+        return filters;
+    }
+
+    render() {
+        const applicansHTML = this.state.applicants.map((a, i) => (<ApplicantExtendedCard key={i} applicant={a} shift={a.shift} hover={true} />));
+        return (<div className="p-1 listcontents">
+            <Theme.Consumer>
+                {({ bar }) => (<span>
+                    {/* <Wizard continuous
+                        steps={this.state.steps}
+                        run={this.state.runTutorial}
+                        callback={callback}
+                    /> */}
+                    <h1><span id="applicant_details_header">Applicant Details</span></h1>
+                    {
+                        (applicansHTML.length == 0) ?
+                            <p>No applicants were found</p>
+                            :
+                            applicansHTML
+                    }
+                </span>)}
+            </Theme.Consumer>
+           
+        </div>);
+    }
+}
+
+
+/**
+ * Applican Card
+ */
+export const ApplicantExtendedCard = (props) => {
+    const startDate = props.shift.starting_at.format('ll');
+    const startTime = props.shift.starting_at.format('LT');
+    const endTime = props.shift.ending_at.format('LT');
+    return (<Theme.Consumer>
+        {({ bar }) =>
+            (<li className="aplicantcard"
+                onClick={() => bar.show({ slug: "show_single_applicant", data: props.applicant.employee, title: "Application Details" })}
+            >
+                <Avatar url={props.applicant.employee.user.profile.picture} />
+                <AcceptReject
+                    onAccept={() => acceptCandidate(props.shift.id, props.applicant.employee).then(() => props.onAccept ? props.onAccept() : null)}
+                    onReject={() => rejectCandidate(props.shift.id, props.applicant.employee).then(() => props.onReject ? props.onReject() : null)}
+                />
+                <p>
+                    <a href="#" className="shift-position">{props.applicant.employee.user.first_name + " " + props.applicant.employee.user.last_name} </a>
+                    is applying for the {props.shift.position.title} position
+                    at the <a href="#" className="shift-location"> {props.shift.venue.title}</a>
+                    <span className="shift-date"> {startDate} from {startTime} to {endTime} </span>
+                    {
+                        (typeof props.shift.price == 'string') ?
+                            <span className="shift-price"> ${props.shift.price}/hr.</span>
+                            :
+                            <span className="shift-price"> {props.shift.price.currencySymbol}{props.shift.price.amount}/{props.shift.price.timeframe}.</span>
+                    }
+                </p>
+                <Stars rating={Number(props.applicant.employee.rating)} />
+            </li>)}
+    </Theme.Consumer>);
+};
+ApplicantExtendedCard.propTypes = {
+    applicant: PropTypes.object.isRequired,
+    onAccept: PropTypes.func,
+    onReject: PropTypes.func,
+    shift: PropTypes.object.isRequired
+};
+ApplicantExtendedCard.defaultProps = {
+    onAccept: null,
+    onReject: null
+};
+
+/**
+ * Application Details
+ */
+export const ApplicationDetails = (props) => {
+    const applicant = props.catalog.applicant.employee || props.catalog.applicant;
+    return (<Theme.Consumer>
+        {({ bar }) =>
+            (<li className="aplication-details">
+                <Avatar url={applicant.user.profile.picture} />
+                <p>{applicant.user.first_name + " " + applicant.user.last_name}</p>
+                <Stars rating={Number(applicant.rating)} />
+                <span>Doing 4 jobs</span>
+                <p>$ 13 /hr Minimum Rate</p>
+                <p>{applicant.user.profile.bio}</p>
+            </li>)}
+    </Theme.Consumer>);
+};
+ApplicationDetails.propTypes = {
+    onSave: PropTypes.func.isRequired,
+    catalog: PropTypes.object.isRequired
+};
+
+
+/**
+ * Filter Applications
+ */
+export const FilterApplications = ({ onSave, onCancel, onChange, catalog, formData }) => {
+return(
+    <form>
+        <div className="row">
+            <div className="col">
+                <label>Looking for</label>
+                <Select isMulti
+                    value={formData.positions}
+                    options={catalog.positions}
+                    onChange={(selection) => onChange({ positions: selection })}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col">
+                <label>Price / hour</label>
+                <input type="number" className="form-control" onChange={(e) => onChange({ minimum_hourly_rate: e.target.value })} value={formData.minimum_hourly_rate} />
+            </div>
+            <div className="col">
+                <label>Date</label>
+                <input type="date" className="form-control" onChange={(e) => onChange({ date: e.target.value })} />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col">
+                <label>Venue</label>
+                <Select isMulti
+                    value={formData.venues}
+                    options={catalog.venues}
+                    onChange={(selection) => onChange({ venues: selection })}
+                />
+            </div>
+        </div>
+        <div className="btn-bar">
+            <button type="button" className="btn btn-primary" onClick={() => onSave()}>Apply Filters</button>
+            <button type="button" className="btn btn-secondary" onClick={() => {
+                    formData.venues = [];
+                    formData.positions = [];
+                    formData.date = '';
+                    formData.minimum_hourly_rate = '';
+                    onSave(false);
+            }}>Clear Filters</button>
+        </div>
+    </form>
+);
+};
+FilterApplications.propTypes = {
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    formData: PropTypes.object, //contains the data needed for the form to load
+    catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_deductions.js.html b/docs/views_deductions.js.html new file mode 100644 index 0000000..64fcd67 --- /dev/null +++ b/docs/views_deductions.js.html @@ -0,0 +1,252 @@ + + + + + JSDoc: Source: views/deductions.js + + + + + + + + + + +
+ +

Source: views/deductions.js

+ + + + + + +
+
+
import React from "react";
+import { validator, ValidationError } from '../utils/validation';
+import PropTypes from 'prop-types';
+import Select from 'react-select';
+
+export const Deduction = (data = {}) => {
+
+    const _defaults = {
+        id: null,
+        name: '',
+        value: null,
+        description: '',
+        type: '',
+        lock: false,
+        serialize: function () {
+
+            const newDeduction = {
+            };
+
+            return Object.assign(this, newDeduction);
+        }
+    };
+
+    let _deduction = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+            if (validator.isEmpty(_deduction.name)) throw new ValidationError('The deduction name cannot be empty');
+            if (!_deduction.value) throw new ValidationError('Deduction cannot be empty');
+            if (validator.isEmpty(_deduction.description)) throw new ValidationError('The deduction description cannot be empty');
+            if (!_deduction.type) throw new ValidationError('The deduction type cannot be empty');
+            return _deduction;
+        },
+        defaults: () => {
+            return _defaults;
+        }
+    };
+};
+/**
+ * Create deduction
+ */
+export const CreateDeduction = ({ 
+    onSave, 
+    onCancel, 
+    onChange, 
+    catalog, 
+    formData, 
+    bar, 
+    error
+ }) => {
+        return ( <form>
+            <div className="row">
+                <div className="col-6">
+                    <label>Name:</label>
+                    <input className="form-control"
+                        value={formData.name}
+                        onChange={(e)=> onChange({ name: e.target.value })}
+                    />
+                </div>
+                <div className="col-6">
+                    <label>Deduction</label>
+                    <input 
+                        type="number" 
+                        className="form-control"
+                        placeholder='0.00'
+                        value={formData.value}
+                        onChange={(e)=> onChange({value: e.target.value > 0 ? Number(e.target.value).toFixed(2) : ''})}
+                    />
+                </div>
+            </div>
+            <div className="row">
+                <div className="col-12">
+                    <label>Description:</label>
+                    <input className="form-control"
+                        value={formData.description}
+                        onChange={(e)=> onChange({ description: e.target.value })}
+                    />
+                </div>
+            </div>
+            {/* <div className="row">
+                <div className="col-1" style={{ flexDirection: "row", display: "flex", alignItems: "center" }}>
+                    <input type="checkbox"
+                        style={{ marginRight: "6px" }}
+                        checked={formData.lock}
+                        onChange={(e)=> onChange({ lock: e.target.checked})}
+                    />
+                    <label>Lock </label>
+                </div>
+            </div> */}
+            <div className="row">
+                <div className="col-12">
+                    <label>Deduction type:</label>
+                    <Select
+                        value={catalog.deductionsTypes.find((a) => a.value === formData.type)}
+                        onChange={(selection) => onChange({ type: selection.value.toString() })}
+                        options={catalog.deductionsTypes}
+                    />
+                </div>
+            </div>
+            <div className="btn-bar">
+                <button 
+                type="button"
+                className="btn btn-success" 
+                onClick={() => onSave({
+                    name: formData.name,
+                    value: formData.value,
+                    type: formData.type,
+                    description: formData.description
+                })}
+                >
+                    Create
+                </button>
+            </div>
+        </form>);
+};
+
+CreateDeduction.propTypes = {
+    error: PropTypes.string,
+    action: PropTypes.string,
+    bar: PropTypes.object,
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    formData: PropTypes.object,
+    catalog: PropTypes.object //contains the data needed for the form to load
+};
+/**
+ * Edit deduction
+ */
+export const UpdateDeduction = ({
+    onSave, 
+    onCancel, 
+    onChange, 
+    catalog, 
+    formData, 
+    bar, 
+    error
+ }) => {
+        return ( <form>
+            <div className="row">
+                <div className="col-6">
+                    <label>Name:</label>
+                    <input className="form-control"
+                        value={formData.name}
+                        onChange={(e)=> onChange({ name: e.target.value })}
+                    />
+                </div>
+                <div className="col-6">
+                    <label>Deduction</label>
+                    <input type="number" className="form-control"
+                        value={formData.value}
+                        onChange={(e)=> onChange({ value: Number(e.target.value)})}
+                    />
+                </div>
+            </div>
+            <div className="row">
+                <div className="col-12">
+                    <label>Description:</label>
+                    <input className="form-control"
+                        value={formData.description}
+                        onChange={(e)=> onChange({ description: e.target.value })}
+                    />
+                </div>
+            </div>
+            <div className="row">
+                <div className="col-1" style={{ flexDirection: "row", display: "flex", alignItems: "center" }}>
+                    <input 
+                        type="checkbox"
+                        style={{ marginRight: "6px" }}
+                        checked={formData.lock}
+                        onChange={(e)=> onChange({ lock: e.target.checked})}
+                    />
+                    <label>Lock </label>
+                </div>
+            </div>
+            <div className="btn-bar">
+                <button 
+                type="button"
+                className="btn btn-success" 
+                onClick={() => onSave({
+                    id: formData.id,
+                    name: formData.name,
+                    value: formData.value,
+                    lock: formData.lock,
+                    description: formData.description
+                })}
+                >
+                    Save
+                </button>
+            </div>
+        </form>);
+};
+
+UpdateDeduction.propTypes = {
+    error: PropTypes.string,
+    action: PropTypes.string,
+    bar: PropTypes.object,
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    formData: PropTypes.object,
+    catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_favorites.js.html b/docs/views_favorites.js.html new file mode 100644 index 0000000..9fa3ec0 --- /dev/null +++ b/docs/views_favorites.js.html @@ -0,0 +1,566 @@ + + + + + JSDoc: Source: views/favorites.js + + + + + + + + + + +
+ +

Source: views/favorites.js

+ + + + + + +
+
+
import React from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from "prop-types";
+import {
+  store,
+  update,
+  remove,
+  updateTalentList,
+  fetchAllMe,
+  searchMe,
+} from "../actions.js";
+import { callback, hasTutorial } from "../utils/tutorial";
+import {
+  ListCard,
+  EmployeeExtendedCard,
+  Button,
+  Theme,
+  Wizard,
+  SearchCatalogSelect,
+  GenericCard,
+} from "../components/index";
+import Select from "react-select";
+import { Session } from "bc-react-session";
+import { Notify } from "bc-react-notifier";
+import { GET } from "../utils/api_wrapper";
+
+export const Favlist = (data) => {
+  const _defaults = {
+    title: "",
+    auto_accept_employees_on_this_list: true,
+    employees: [],
+    employer: Session.getPayload().user.profile.employer,
+    serialize: function (filters = []) {
+      const newEntity = {
+        id: data.id,
+        employees: _defaults.employees.map((emp) => emp.id || emp),
+      };
+      let response = Object.assign(this, newEntity);
+
+      filters.forEach((property) => delete response[property]);
+      return response;
+    },
+  };
+
+  let _entity = Object.assign(_defaults, data);
+  return {
+    validate: () => {
+      // if(!validator.isEmail(_entity.email)) throw new ValidationError('Please specify the email');
+      // if(validator.isEmpty(_entity.first_name)) throw new ValidationError('Please specify the first name');
+      // if(validator.isEmpty(_entity.last_name)) throw new ValidationError('Please specify the last name');
+      return _entity;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      let _formShift = {
+        id: _entity.id,
+        title: _entity.title,
+        auto_accept_employees_on_this_list:
+          _entity.auto_accept_employees_on_this_list,
+        employees: _entity.employees,
+      };
+      return _formShift;
+    },
+    filters: () => {
+      const _filters = {
+        // positions: _entity.positions.map( item => item.value ),
+      };
+      return Object.assign(_entity, _filters);
+    },
+  };
+};
+
+export class ManageFavorites extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      lists: [],
+    };
+  }
+
+  componentDidMount() {
+    const lists = store.getState("favlists");
+    this.subscribe(store, "favlists", (lists) => {
+      this.setState({ lists });
+    });
+    this.setState({ lists: lists ? lists : [], runTutorial: true });
+
+    fetchAllMe(["favlists"]);
+  }
+
+  render() {
+    return (
+      <div className="p-1 listcontents">
+        <h1>
+          <span id="your-favorites-heading">Your Favorite List:</span>
+        </h1>
+        <Theme.Consumer>
+          {({ bar }) =>
+            this.state.lists.length == 0 ? (
+              <p>
+                You have no favorite lists yet,{" "}
+                <a
+                  href="#"
+                  className="text-primary"
+                  onClick={() => bar.show({ slug: "create_favlist" })}
+                >
+                  click here
+                </a>{" "}
+                to create your first
+              </p>
+            ) : (
+              this.state.lists
+                .sort((a, b) => a.title.localeCompare(b.title))
+                .map((list, i) => (
+                  <ListCard
+                    key={i}
+                    list={list}
+                    onClick={() =>
+                      bar.show({
+                        slug: "favlist_employees",
+                        data: list,
+                        title: "List Details",
+                      })
+                    }
+                  >
+                    <button
+                      type="button"
+                      className="btn btn-secondary"
+                      style={{ background: "transparent" }}
+                      onClick={(e) => {
+                        e.stopPropagation();
+                        bar.show({
+                          slug: "update_favlist",
+                          data: list,
+                          title: "List Details",
+                        });
+                      }}
+                    >
+                      <i className="fas fa-pencil-alt"></i>
+                    </button>
+                    <button
+                      type="button"
+                      className="btn btn-secondary"
+                      style={{ background: "transparent" }}
+                      onClick={(e) => {
+                        e.stopPropagation();
+                        const noti = Notify.info("Are you sure?", (answer) => {
+                          if (answer) remove("favlists", list);
+                          noti.remove();
+                        });
+                      }}
+                    >
+                      <i className="fas fa-trash-alt"></i>
+                    </button>
+                  </ListCard>
+                ))
+            )
+          }
+        </Theme.Consumer>
+        
+      </div>
+    );
+  }
+}
+
+/**
+ * Add To Favorite List
+ */
+export const AddFavlistsToTalent = ({
+  onChange,
+  formData,
+  onSave,
+  onCancel,
+  catalog,
+}) => {
+  return (
+    <Theme.Consumer>
+      {({ bar }) => (
+        <form>
+          <div className="row">
+            <div className="col-12">
+              <label>Pick your favorite lists:</label>
+              <Select
+                isMulti
+                className="select-favlists"
+                value={formData.favoriteLists}
+                options={[
+                  {
+                    label: "Add new favorite list",
+                    value: "new_favlist",
+                    component: AddorUpdateFavlist,
+                  },
+                ].concat(catalog.favlists)}
+                onChange={(selection) => {
+                  const create = selection.find(
+                    (opt) => opt.value == "new_favlist"
+                  );
+                  if (create)
+                    bar.show({ slug: "create_favlist", allowLevels: true });
+                  else onChange({ favoriteLists: selection });
+                }}
+              />
+            </div>
+          </div>
+          <p>Click on invite add the talent to your favorite lists</p>
+          <div className="btn-bar">
+            <Button color="primary" onClick={() => onSave()}>
+              Save
+            </Button>
+            <Button color="secondary" onClick={() => onCancel()}>
+              Cancel
+            </Button>
+          </div>
+        </form>
+      )}
+    </Theme.Consumer>
+  );
+};
+
+AddFavlistsToTalent.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+export const AddorUpdateFavlist = ({
+  formData,
+  onChange,
+  onSave,
+  onCancel,
+}) => (
+  <form>
+    <div className="row">
+      <div className="col-12">
+        <label>List name:</label>
+        <input
+          type="text"
+          className="form-control"
+          placeholder="List name"
+          value={formData.title}
+          onChange={(e) => onChange({ title: e.target.value })}
+        />
+      </div>
+    </div>
+    <div className="row mt-1">
+      <div className="col-1 text-center">
+        <input
+          type="checkbox"
+          placeholder="List name"
+          checked={formData.auto_accept_employees_on_this_list}
+          onChange={(e) =>
+            onChange({ auto_accept_employees_on_this_list: e.target.checked })
+          }
+        />
+      </div>
+      <div className="col-11">
+        Talents on this list do not require approval to join shifts
+      </div>
+    </div>
+    <div className="btn-bar">
+      <Button color="light" onClick={() => onCancel()}>
+        Cancel
+      </Button>
+      <Button color="success" className="ml-2" onClick={() => onSave()}>
+        Save
+      </Button>
+    </div>
+  </form>
+);
+AddorUpdateFavlist.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  formData: PropTypes.object.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+export const FavlistEmployees = ({ formData, onChange, onSave, catalog }) => {
+  const favlist = store.get("favlists", formData.id);
+  return (
+    <form>
+      <Theme.Consumer>
+        {({ bar }) => (
+          <span>
+            <div className="top-bar">
+              <button
+                type="button"
+                className="btn btn-primary btn-sm rounded"
+                onClick={() =>
+                  bar.show({
+                    slug: "update_favlist",
+                    data: formData,
+                    allowLevels: true,
+                  })
+                }
+              >
+                <i className="fas fa-pencil-alt"></i>
+              </button>
+              <button
+                type="button"
+                className="btn btn-secondary btn-sm rounded"
+                onClick={() => onChange({ _mode: "add_talent" })}
+              >
+                <i className="fas fa-plus"></i>
+              </button>
+            </div>
+            <div className="row">
+              <div className="col-12">
+                <label>Title: </label>
+                <span>{favlist.title}</span>
+              </div>
+            </div>
+            {typeof formData._mode == "undefined" ||
+            formData._mode == "default" ? (
+              <div className="row">
+                <div className="col-12">
+                  <label>Talents: </label>
+                  {!favlist || favlist.employees.length == 0 ? (
+                    <span>
+                      There are no talents in this list yet,{" "}
+                      <span
+                        className="anchor"
+                        onClick={() => onChange({ _mode: "add_talent" })}
+                      >
+                        click here to add a new one
+                      </span>
+                    </span>
+                  ) : (
+                    ""
+                  )}
+                </div>
+              </div>
+            ) : (
+              ""
+            )}
+            {typeof formData._mode != "undefined" &&
+            formData._mode == "add_talent" ? (
+              <div className="row mb-2">
+                <div className="col-12">
+                  <label>Search for the talents you want to add</label>
+                  <SearchCatalogSelect
+                    isMulti={false}
+                    onChange={(selection) => {
+                      if (selection.value == "invite_talent_to_jobcore")
+                        bar.show({
+                          allowLevels: true,
+                          slug: "invite_talent_to_jobcore",
+                        });
+                      else
+                        updateTalentList("add", selection.value, formData.id)
+                          .then(() => onChange({ _mode: "default" }))
+                          .catch((error) => alert(error));
+                    }}
+                    searchFunction={(search) =>
+                      new Promise((resolve, reject) =>
+                        GET("catalog/employees?full_name=" + search)
+                          .then((talents) =>
+                            resolve(
+                              [
+                                {
+                                  label: `${
+                                    talents.length == 0 ? "No one found: " : ""
+                                  }Invite "${search}" to JobCore?`,
+                                  value: "invite_talent_to_jobcore",
+                                },
+                              ].concat(talents)
+                            )
+                          )
+                          .catch((error) => reject(error))
+                      )
+                    }
+                  />
+                </div>
+              </div>
+            ) : (
+              ""
+            )}
+            {favlist && favlist.employees.length > 0 ? (
+              <div className="row">
+                <div className="col-12">
+                  <ul
+                    className="scroll"
+                    style={{
+                      maxHeight: "600px",
+                      overflowY: "auto",
+                      padding: "10px",
+                      margin: "-10px",
+                    }}
+                  >
+                    {favlist.employees.map((em, i) => (
+                      <EmployeeExtendedCard
+                        key={i}
+                        employee={em}
+                        hover={false}
+                        showFavlist={false}
+                        onClick={() =>
+                          bar.show({
+                            slug: "show_single_talent",
+                            data: em,
+                            allowLevels: true,
+                          })
+                        }
+                      >
+                        <Button
+                          className="mt-0"
+                          icon="trash"
+                          label="Delete"
+                          onClick={() => {
+                            updateTalentList("delete", em.id, formData.id);
+                            //.then(() => onChange);
+                          }}
+                        />
+                      </EmployeeExtendedCard>
+                    ))}
+                  </ul>
+                </div>
+              </div>
+            ) : (
+              ""
+            )}
+          </span>
+        )}
+      </Theme.Consumer>
+    </form>
+  );
+};
+FavlistEmployees.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  formData: PropTypes.object.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+export class ManagePayrates extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      locations: [],
+    };
+  }
+
+  componentDidMount() {
+    this.filter();
+    this.subscribe(store, "venues", (locations) => {
+      this.setState({ locations });
+    });
+
+    this.props.history.listen(() => {
+      this.filter();
+      this.setState({ firstSearch: false });
+    });
+  }
+
+  filter(locations = null) {
+    searchMe("venues", window.location.search);
+  }
+
+  render() {
+    if (this.state.firstSearch) return <p>Search for any location</p>;
+    const allowLevels = window.location.search != "";
+    return (
+      <div className="p-1 listcontents">
+        <Theme.Consumer>
+          {({ bar }) => (
+            <span>
+              <h1>
+                <span id="talent_search_header">Payrates Search</span>
+              </h1>
+              {this.state.locations.map((l, i) => (
+                <GenericCard
+                  key={i}
+                  hover={true}
+                  onClick={() =>
+                    bar.show({ slug: "update_location", data: l, allowLevels })
+                  }
+                >
+                  <div className="btn-group">
+                    <Button
+                      icon="pencil"
+                      onClick={() =>
+                        bar.show({
+                          slug: "update_location",
+                          data: l,
+                          allowLevels,
+                        })
+                      }
+                    ></Button>
+                    <Button
+                      icon="trash"
+                      onClick={() => {
+                        const noti = Notify.info(
+                          "Are you sure you want to delete this location?",
+                          (answer) => {
+                            if (answer) remove("venues", l);
+                            noti.remove();
+                          }
+                        );
+                      }}
+                    ></Button>
+                  </div>
+                  <p className="mt-2">{l.title}</p>
+                </GenericCard>
+              ))}
+            </span>
+          )}
+        </Theme.Consumer>
+      </div>
+    );
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_invites.js.html b/docs/views_invites.js.html new file mode 100644 index 0000000..035be2d --- /dev/null +++ b/docs/views_invites.js.html @@ -0,0 +1,332 @@ + + + + + JSDoc: Source: views/invites.js + + + + + + + + + + +
+ +

Source: views/invites.js

+ + + + + + +
+
+
import React, { useEffect, useState } from 'react';
+import {validator, ValidationError} from '../utils/validation';
+import {update, searchMe} from '../actions';
+import {Session} from 'bc-react-session';
+import PropTypes from 'prop-types';
+import {GET} from '../utils/api_wrapper';
+import Select from 'react-select';
+import {TIME_FORMAT, DATE_FORMAT, NOW} from '../components/utils.js';
+import {Button, Theme, ShiftOption, ShiftOptionSelected, SearchCatalogSelect, ShiftCard} from '../components/index';
+import {Shift} from "./shifts.js";
+import moment from 'moment';
+
+export const Invite = (data) => {
+
+    const _defaults = {
+        first_name: '',
+        last_name: '',
+        status: 'PENDING',
+        created_at: NOW(),
+        email: '',
+        include_sms: undefined,
+        phone_number: '',
+        serialize: function(){
+
+            const newShift = {
+                sender: Session.getPayload().user.profile.id
+            };
+
+            return Object.assign(this, newShift);
+        },
+        unserialize: function(){
+            const dataType = typeof this.created_at;
+            //if its already serialized
+            if((typeof this.position == 'object') && ['number','string'].indexOf(dataType) == -1) return this;
+            const newInvite = {
+                shift: Shift(this.shift).defaults().unserialize(),
+                created_at: (!moment.isMoment(this.created_at)) ? moment(this.created_at) : this.created_at
+            };
+
+            return Object.assign(this, newInvite);
+        }
+    };
+
+    let _entity = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+            if(!validator.isEmail(_entity.email)) throw new ValidationError('Please specify the email');
+            if(validator.isEmpty(_entity.first_name)) throw new ValidationError('Please specify the first name');
+            if(validator.isEmpty(_entity.last_name)) throw new ValidationError('Please specify the last name');
+            //if(validator.isEmpty(_entity.phone_number.toString())) throw new ValidationError('Please specify the last name');
+            return _entity;
+        },
+        defaults: () => {
+            return _defaults;
+        },
+        getFormData: () => {
+            const _formShift = {
+                //foo: _entity.bar
+            };
+            return _formShift;
+        },
+        filters: () => {
+            const _filters = {
+                positions: _entity.positions.map( item => item.value ),
+                badges: _entity.badges.map( item => item.value )
+            };
+            for(let key in _entity) if(typeof _entity[key] == 'function') delete _entity[key];
+            return Object.assign(_entity, _filters);
+        }
+    };
+};
+
+/**
+ * AddShift
+ */
+export const SearchShiftToInviteTalent = (props) => {
+
+    const [ shifts, setShifts ] = useState([]);
+    useEffect(() => {
+        searchMe('shifts', `?upcoming=true&employee_not=${props.formData.employees.join(',')}&employee_not=${props.formData.employees.join(',')}`)
+            .then(data => setShifts(data.map(item => ({ value: Shift(item).defaults().unserialize(), label: '' }))));
+    }, []);
+
+    return (<form>
+        <div className="row">
+            <div className="col-12">
+                <label>Pick your shifts:</label>
+                <Select isMulti className="select-shifts"
+                    value={props.formData.shifts}
+                    components={{ Option: ShiftOption, MultiValue: ShiftOptionSelected({ multi: true }) }}
+                    onChange={(selectedOption)=>props.onChange({ shifts: selectedOption })}
+                    options={shifts}
+                >
+                </Select>
+            </div>
+        </div>
+        <p>Click on invite to invite the talent to your selected shifts</p>
+        <div className="btn-bar">
+            <Button color="primary" onClick={() => props.onSave()}>Send Invite</Button>
+        </div>
+    </form>);
+};
+SearchShiftToInviteTalent.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+/**
+ * Invite Talent To Shift
+ */
+export const SearchTalentToInviteToShift = ({ formData, onSave, onChange }) => {
+    return (<Theme.Consumer>
+        {({bar}) => (<form>
+            <div className="row">
+                <div className="col-12">
+                    <label>Seach the JobCore Database:</label>
+                    <SearchCatalogSelect
+                        isMulti={true}
+                        value={formData.pending_invites}
+                        onChange={(selections)=> {
+                            const invite = selections.find(opt => opt.value == 'invite_talent_to_jobcore');
+                            if(invite) bar.show({
+                                allowLevels: true,
+                                slug: "invite_talent_to_jobcore",
+                                onSave: (emp) => onChange({ pending_jobcore_invites: formData.pending_jobcore_invites.concat(emp) })
+                            });
+                            else onChange({ pending_invites: selections, employees: selections.map(opt => opt.value) });
+                        }}
+                        searchFunction={(search) => new Promise((resolve, reject) =>
+                            GET('catalog/employees?full_name='+search)
+                                .then(talents => resolve([
+                                    { label: `${(talents.length==0) ? 'No one found: ':''}Invite "${search}" to jobcore`, value: 'invite_talent_to_jobcore' }
+                                ].concat(talents)))
+                                .catch(error => reject(error))
+                        )}
+                    />
+                </div>
+            </div>
+            <p>Click on invite to invite the talent to your selected shifts</p>
+            <div className="btn-bar">
+                <Button color="primary" onClick={() => onSave()}>Send Invite</Button>
+            </div>
+        </form>)}
+    </Theme.Consumer>);
+};
+SearchTalentToInviteToShift.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+/**
+ * ShiftDetails
+ */
+export const InviteTalentToJobcore = ({ onSave, onCancel, onChange, catalog, formData }) => (<Theme.Consumer>
+    {({bar}) => (
+        <form id="invite_talent_jobcore">
+            <div className="row">
+                <div className="col-12">
+                    <p>
+                        <span>Invite someone into yor talent pool or </span>
+                        <span className="anchor"
+                            onClick={() => bar.show({ slug: "show_pending_jobcore_invites", allowLevels: true })}
+                        >review previous invites</span>:
+                    </p>
+                </div>
+            </div>
+            <div className="row">
+                <div className="col-12">
+                    <label>Talent First Name</label>
+                    <input type="text" className="form-control"
+                        onChange={(e)=>onChange({first_name: e.target.value})}
+                    />
+                </div>
+                <div className="col-12">
+                    <label>Talent Last Name</label>
+                    <input type="text" className="form-control"
+                        onChange={(e)=>onChange({last_name: e.target.value})}
+                    />
+                </div>
+                <div className="col-12">
+                    <label>Talent email</label>
+                    <input type="email" className="form-control"
+                        onChange={(e)=>onChange({email: e.target.value})}
+                    />
+                </div>
+                <div className="col-12">
+                    <label>Talent Phone</label>
+                    <input type="tel" className="form-control"
+                        onChange={(e)=>onChange({phone_number: e.target.value})}
+                    />
+                    <div className="form-group text-left">
+                        <input type="checkbox" className="mr-1"
+                            onChange={(e) => onChange({ include_sms: !formData.include_sms })} checked={formData.include_sms}
+                        />
+                        Send invite throught SMS as well.
+                    </div>
+                </div>
+            </div>
+            <div className="btn-bar">
+                <Button color="success" onClick={() => onSave()}>Send Invite</Button>
+                <Button color="secondary" onClick={() => onCancel()}>Cancel</Button>
+            </div>
+        </form>
+    )}
+</Theme.Consumer>);
+InviteTalentToJobcore.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  width: PropTypes.number,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+/**
+ * ShiftDetails
+ */
+export const PendingJobcoreInvites = ({ catalog, formData }) => (<div>
+    <div className="row">
+        <div className="col">
+            <h2>These are your pending invites</h2>
+            <ul className="li-white">
+                { (catalog.jcInvites.length > 0) ?
+                    catalog.jcInvites.map((inv, i) =>{
+                        if(inv.status == "PENDING" || inv.status == "COMPANY"){
+                            return(<li key={i}>
+                                <button
+                                    className="btn btn-primary float-right mt-2 btn-sm"
+                                    onClick={() => update('jobcore-invites', inv)}
+                                >Resend</button>
+                                <p className="p-0 m-0">
+                                    <span>{inv.first_name} {inv.last_name} {inv.employer && " (company)"} </span>
+                                </p>
+                                <span className="badge">{moment(inv.updated_at).fromNow()}</span>
+                                <small>{inv.email}</small>
+                            </li>);
+                        }
+                    }
+                    ):
+                    <p className="text-center">No pending invites</p>
+                }
+            </ul>
+        </div>
+    </div>
+</div>);
+PendingJobcoreInvites.propTypes = {
+  width: PropTypes.number,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+/**
+ * ShiftDetails
+ */
+export const PendingInvites = ({ catalog, formData }) => {
+    return (<div>
+        <div className="row">
+            <div className="col">
+                <h2>Pending invites for {formData.talent.user.first_name}</h2>
+                <ul className="li-white mx-1">
+                    { (catalog.invites.length > 0) ?
+                        catalog.invites.map((inv, i) =>
+                            (<ShiftCard key={i} shift={inv.shift} showStatus={false} hoverEffect={false} />)
+                        ):
+                        <p className="text-center">No pending invites</p>
+                    }
+                </ul>
+            </div>
+        </div>
+    </div>);
+};
+PendingInvites.propTypes = {
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_locations.js.html b/docs/views_locations.js.html new file mode 100644 index 0000000..0a8d120 --- /dev/null +++ b/docs/views_locations.js.html @@ -0,0 +1,559 @@ + + + + + JSDoc: Source: views/locations.js + + + + + + + + + + +
+ +

Source: views/locations.js

+ + + + + + +
+
+
import React from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from 'prop-types';
+import {store, searchMe, remove,updateProfileMe} from '../actions.js';
+import { GenericCard, Theme, Button,Wizard } from '../components/index';
+import Select from 'react-select';
+import queryString from 'query-string';
+import {hasTutorial } from '../utils/tutorial';
+import {Link} from "react-router-dom";
+import { Session } from 'bc-react-session';
+
+import {validator, ValidationError} from '../utils/validation';
+
+import {Notify} from 'bc-react-notifier';
+
+import GoogleMapReact from 'google-map-react';
+import markerURL from '../../img/marker.png';
+import PlacesAutocomplete, {geocodeByAddress, getLatLng} from 'react-places-autocomplete';
+
+const ENTITIY_NAME = 'venues';
+
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getTalentInitialFilters = (catalog) => {
+    let query = queryString.parse(window.location.search);
+    if(typeof query == 'undefined') return {};
+    if(!Array.isArray(query.positions)) query.positions = (typeof query.positions == 'undefined') ? [] : [query.positions];
+    if(!Array.isArray(query.badges)) query.badges = (typeof query.badges == 'undefined') ? [] : [query.badges];
+    return {
+        positions: query.positions.map(pId => catalog.positions.find(pos => pos.value == pId)),
+        badges: query.badges.map(bId => catalog.badges.find(b => b.value == bId)),
+        rating: catalog.stars.find(rate => rate.value == query.rating)
+    };
+};
+
+export const Location = (data) => {
+
+    const _defaults = {
+        id: '',
+        title: '',
+        street_address: '',
+        country: '',
+        latitude: 25.7617,
+        longitude: 80.1918,
+        state: '',
+        zip_code: '',
+        serialize: function(){
+
+            const newLocation = {
+                latitude: this.latitude.toFixed(6),
+                longitude: this.longitude.toFixed(6)
+//                status: (this.status == 'UNDEFINED') ? 'DRAFT' : this.status,
+            };
+
+            return Object.assign(this, newLocation);
+        }
+    };
+
+    let _location = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+            if(validator.isEmpty(_location.title)) throw new ValidationError('The location title cannot be empty');
+            if(validator.isEmpty(_location.street_address)) throw new ValidationError('The location address cannot be empty');
+            if(validator.isEmpty(_location.country)) throw new ValidationError('The location country cannot be empty');
+            if(validator.isEmpty(_location.state)) throw new ValidationError('The location state cannot be empty');
+            if(validator.isEmpty(_location.zip_code)) throw new ValidationError('The location zip_code cannot be empty');
+            return _location;
+        },
+        defaults: () => {
+            return _defaults;
+        },
+        getFormData: () => {
+            const _formShift = {
+                id: _location.id.toString(),
+                title: _location.title,
+                street_address: _location.street_address,
+                latitude: parseFloat(_location.latitude),
+                longitude: parseFloat(_location.longitude),
+                country: _location.country,
+                state: _location.state,
+                date: _location.zip_code
+            };
+            return _formShift;
+        }
+    };
+};
+
+export class ManageLocations extends Flux.DashView {
+
+    constructor(){
+        super();
+        this.state = {
+            locations: [],
+            currentUser: Session.getPayload().user.profile,
+            runTutorial: hasTutorial(),
+            steps: [
+                {
+                    content: <div><h2>Location page</h2><p>Here you will find all your company locations. You always can add new locations later.</p></div>,
+                    placement: "center",   
+                    disableBeacon: true,
+
+                    styles: {
+                        options: {
+                            zIndex: 10000
+                        },
+                        buttonClose: {
+                            display: "none"
+                        }
+                    },
+                    locale: { skip: "Skip tutorial" },
+                    target: "body",
+                    disableCloseOnEsc: true,
+                    spotlightClicks: true
+                    },
+                {
+                    target: '#create_location',
+                    content: "Let's start by creating your new location",
+                    placement: 'right',
+                    styles: {
+                        buttonClose: {
+                            display: "none"
+                        },
+                        buttonNext: {
+                            display: 'none'
+                        }
+                    },
+                    spotlightClicks: true
+
+                },
+                {
+                    target: '#payroll',
+                    content: 'Set up the company payroll',
+                    placement: 'right',
+                    styles: {
+                        buttonClose: {
+                            display: "none"
+                        },
+                        buttonNext: {
+                            display: 'none'
+                        }
+                    },
+                    spotlightClicks: true
+                }
+            ]
+        };
+    }
+
+    componentDidMount(){
+
+        this.filter();
+        this.subscribe(store, ENTITIY_NAME, (locations) => {
+            this.setState({ locations });
+        });
+
+        this.props.history.listen(() => {
+            this.filter();
+            this.setState({ firstSearch: false });
+        });
+    }
+
+    filter(locations=null){
+        searchMe(ENTITIY_NAME, window.location.search);
+    }
+
+    render() {
+        if(this.state.firstSearch) return <p>Search for any location</p>;
+        const allowLevels = (window.location.search != '');
+        console.log(this.state);
+        return (<div className="p-1 listcontents">
+            <Theme.Consumer>
+                {({bar}) => (<span>
+                    <Wizard continuous
+                            steps={this.state.steps}
+                            run={this.state.runTutorial}
+                            callback={callback}
+                            allowClicksThruHole= {true}
+                            disableOverlay= {true}
+                            spotlightClicks= {true}
+                            styles={{
+                                options: {
+                                  primaryColor: '#000'
+                                }
+                              }}
+                        />
+                    <h1><span id="talent_search_header">Location Search</span></h1>
+                    {this.state.locations.map((l,i) => (
+                        <GenericCard key={i} hover={true} onClick={() => bar.show({ slug: "update_location", data: l, allowLevels })}>
+                            <div className="btn-group">
+                                <Button  icon="pencil" onClick={() => bar.show({ slug: "update_location", data: l, allowLevels })}></Button>
+                                <Button  icon="trash" onClick={() => {
+                                    const noti = Notify.info("Are you sure you want to delete this location?",(answer) => {
+                                        if(answer) remove('venues', l);
+                                        noti.remove();
+                                    });
+                                }}></Button>
+                            </div>
+                            <p className="mt-2">{l.title}</p>
+                        </GenericCard>
+                    ))}
+                </span>)}
+            </Theme.Consumer>
+        </div>);
+    }
+}
+
+/**
+ * AddShift
+ */
+export const FilterLocations = (props) => {
+    return (<form>
+        <div className="row">
+            <div className="col-6">
+                <label>First Name:</label>
+                <input className="form-control"
+                    value={props.formData.first_name}
+                    onChange={(e)=>props.onChange({ first_name: e.target.value })}
+                />
+            </div>
+            <div className="col-6">
+                <label>Last Name:</label>
+                <input className="form-control"
+                    value={props.formData.last_name}
+                    onChange={(e)=>props.onChange({ last_name: e.target.value })}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-12">
+                <label>Experience in past positions:</label>
+                <Select isMulti
+                    value={props.formData.positions}
+                    onChange={(selectedOption)=>props.onChange({positions: selectedOption})}
+                    options={props.catalog.positions}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-12">
+                <label>Badges:</label>
+                <Select isMulti
+                    value={props.formData.badges}
+                    onChange={(selectedOption)=>props.onChange({badges: selectedOption})}
+                    options={props.catalog.badges}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-12">
+                <label>Minimum start rating</label>
+                <Select
+                    value={props.formData.rating}
+                    onChange={(opt)=>props.onChange({rating: opt})}
+                    options={props.catalog.stars}
+                />
+            </div>
+        </div>
+        <div className="btn-bar">
+            <Button color="primary" onClick={() => props.onSave()}>Apply Filters</Button>
+            <Button color="secondary" onClick={() => props.onSave(false)}>Clear Filters</Button>
+        </div>
+    </form>);
+};
+FilterLocations.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+function callback (data) {
+    console.log('DATA', data);
+ 
+    // if(data.action == 'next' && data.index == 0){
+    //     this.props.history.push("/payroll");
+
+    // }
+    // if(data.type == 'tour:end'){
+    //     const session = Session.get();
+    //     updateProfileMe({show_tutorial: false});
+        
+    //     const profile = Object.assign(session.payload.user.profile, { show_tutorial: false });
+    //     const user = Object.assign(session.payload.user, { profile });
+    //     Session.setPayload({ user });
+    // }
+    if(data.action == 'skip'){
+        const session = Session.get();
+        updateProfileMe({show_tutorial: false});
+        
+        const profile = Object.assign(session.payload.user.profile, { show_tutorial: false });
+        const user = Object.assign(session.payload.user, { profile });
+        Session.setPayload({ user });
+    }
+}
+/**
+ * Add a Location
+ */
+function createMapOptions(maps) {
+  // next props are exposed at maps
+  // "Animation", "ControlPosition", "MapTypeControlStyle", "MapTypeId",
+  // "NavigationControlStyle", "ScaleControlStyle", "StrokePosition", "SymbolPath", "ZoomControlStyle",
+  // "DirectionsStatus", "DirectionsTravelMode", "DirectionsUnitSystem", "DistanceMatrixStatus",
+  // "DistanceMatrixElementStatus", "ElevationStatus", "GeocoderLocationType", "GeocoderStatus", "KmlLayerStatus",
+  // "MaxZoomStatus", "StreetViewStatus", "TransitMode", "TransitRoutePreference", "TravelMode", "UnitSystem"
+  return {
+    zoomControlOptions: {
+      position: maps.ControlPosition.RIGHT_CENTER,
+      style: maps.ZoomControlStyle.SMALL
+    },
+    zoomControl: true,
+    scaleControl: false,
+    fullscreenControl: false,
+    mapTypeControl: false
+  };
+}
+const Marker = ({ text }) => (<div><img style={{maxWidth: "25px"}} src={markerURL} /></div>);
+Marker.propTypes = {
+    text: PropTypes.string
+};
+export const AddOrEditLocation = ({onSave, onCancel, onChange, catalog, formData}) => (<Theme.Consumer>
+    {({bar}) => (<div>
+  
+        <div className="row">
+            <Wizard continuous
+                            steps={ [
+                                {
+                                    target: '#address',
+                                    content: 'Start by entering your first address here. This is where your employee will report to work. Note employees will be able to clock in/out within a certain radius of this location.',
+                                    placement: 'left',
+                                    disableBeacon: true,
+                                    styles: {
+                                        options: {
+                                            zIndex: 10000
+                                        },
+                                        buttonClose: {
+                                            display: "none"
+                                        }
+                                    },
+                                    spotlightClicks: true
+
+                                },
+                                {
+                                    target: '#location_nickname',
+                                    content: 'Add a nickname for your location. Note this only for your reference employee will not see the nickname.',
+                                    placement: 'left',
+                                    styles: {
+                                        options: {
+                                            zIndex: 10000
+                                        },
+                                        buttonClose: {
+                                            display: "none"
+                                        }
+                                    },
+                                    spotlightClicks: true
+                                },
+                                {
+                                    target: '#location_details',
+                                    content: 'Finalize location details',
+                                    placement: 'left',
+                                    styles: {
+                                        options: {
+                                            zIndex: 10000
+                                        },
+                                        buttonClose: {
+                                            display: "none"
+                                        }
+                                    },
+                                    spotlightClicks: true
+                                },
+                                {
+                                    target: '#button_save',
+                                    content: "Save your location information",
+                                    placement: 'left',
+                                    styles: {
+                                        options: {
+                                            zIndex: 10000
+                                        },
+                                        buttonClose: {
+                                            display: "none"
+                                        },
+                                        buttonNext: {
+                                            display: 'none'
+                                        }
+                                    },
+                                     spotlightClicks: true
+                                }
+                            ]}
+                            run={hasTutorial()}
+                            callback={callback}
+                            spotlightClicks={true}
+                            styles={{
+                                options: {
+                                  primaryColor: '#000'
+                                }
+                              }}
+
+                        />  
+            <div className="col-12" id="address">
+                <label>Address</label>
+                <PlacesAutocomplete
+                    value={formData.street_address || ''}
+                    onChange={(value)=>onChange({ street_address: value })}
+                    onSelect={(address) => {
+                        onChange({ street_address: address });
+                        geocodeByAddress(address)
+                          .then(results => {
+                                const title = address.split(',')[0];
+                                const pieces = results[0].address_components;
+                                const getPiece = (name) => pieces.find((comp) => typeof comp.types.find(type => type == name) != 'undefined');
+                                const country = getPiece('country');
+                                const state = getPiece('administrative_area_level_1');
+                                const zipcode = getPiece('postal_code');
+                                onChange({ title, country: country.long_name, state: state.long_name, zip_code: zipcode.long_name });
+                                return getLatLng(results[0]);
+                          })
+                          .then(coord => onChange({ latitude: coord.lat, longitude: coord.lng }))
+                          .catch(error => Notify.error('There was an error obtaining the location coordinates'));
+                    }}
+                >
+                    {({ getInputProps, getSuggestionItemProps, suggestions, loading }) => (
+                        <div className="autocomplete-root">
+                            <input {...getInputProps()} className="form-control" autoComplete="new-jobcore-location-employer"/>
+                            <div className="autocomplete-dropdown-container bg-white">
+                                {loading && <div>Loading...</div>}
+                                {suggestions.map((suggestion,i) => (
+                                    <div key={i} {...getSuggestionItemProps(suggestion)} className="p-2">
+                                        <span>{suggestion.description}</span>
+                                    </div>
+                                ))}
+                            </div>
+                        </div>
+                    )}
+                </PlacesAutocomplete>
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-12" id="location_nickname">
+                <label>Location nickname</label>
+                <input type="text" className="form-control"
+                    value={formData.title}
+                    onChange={(e)=>onChange({title: e.target.value})}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-6 pr-0">
+                <label>Location</label>
+                <div className="location-map">
+                    <GoogleMapReact
+                        bootstrapURLKeys={{ key: process.env.GOOGLE_MAPS_WEB_KEY }}
+                        defaultCenter={{
+                          lat: 25.7617,
+                          lng: -80.1918
+                        }}
+                        width="100%"
+                        height="100%"
+                        center={{
+                          lat: formData.latitude,
+                          lng: formData.longitude
+                        }}
+                        options={createMapOptions}
+                        defaultZoom={12}
+                    >
+                        <Marker
+                            lat={formData.latitude}
+                            lng={formData.longitude}
+                            text={'Jobcore'}
+                        />
+                    </GoogleMapReact>
+                </div>
+            </div>
+            <div className="col-6" id="location_details">
+                <label>Country</label>
+                <input type="text" className="form-control"
+                    value={formData.country}
+                    onChange={(e)=>onChange({country: e.target.value})}
+                />
+                <label>State</label>
+                <input type="text" className="form-control"
+                    value={formData.state}
+                    onChange={(e)=>onChange({state: e.target.value})}
+                />
+                <label>Zip</label>
+                <input type="number" className="form-control"
+                    value={formData.zip_code}
+                    onChange={(e)=>onChange({zip_code: e.target.value})}
+                />
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-12">
+                <div className="btn-bar">
+                    {hasTutorial() == true ? (
+                        <Link to ="/payroll/settings" onClick={()=> onSave()}>
+                            <button id="button_save"type="button" className="btn btn-success">Save</button>
+                        </Link>
+                    ): (<button id="button_save"type="button" className="btn btn-success" onClick={() => onSave()}>Save</button>)}
+                    <button type="button" className="btn btn-default" onClick={() => bar.close()}>Cancel</button>
+                </div>
+            </div>
+        </div>
+    </div>)}
+</Theme.Consumer>);
+AddOrEditLocation.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_charts.js.html b/docs/views_metrics_charts.js.html new file mode 100644 index 0000000..cf15252 --- /dev/null +++ b/docs/views_metrics_charts.js.html @@ -0,0 +1,112 @@ + + + + + JSDoc: Source: views/metrics/charts.js + + + + + + + + + + +
+ +

Source: views/metrics/charts.js

+ + + + + + +
+
+
import React from 'react';
+import { Pie, Bar } from 'react-chartjs-2';
+import { Chart as ChartJS } from 'chart.js/auto';
+
+/**
+ * @function
+ * @description Creates a pie chart with the data passed as an argument.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires Pie
+ * @param {object} pieData - Object with data like colors, labels, and values needed for the chart.
+ */
+export const PieChart = ({ pieData }) => {
+  return (
+    <Pie
+      data={pieData}
+      options={{
+        responsive: true,
+        maintainAspectRatio: false,
+        cutout: "0%",
+        animation: {
+          animateScale: true,
+          animateRotate: true,
+        }
+      }}
+    />
+  )
+}
+
+/**
+ * @function
+ * @description Creates a bar chart with the data passed as an argument
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires Bar
+ * @param {object} barData - Object with data like colors, labels, and values needed for the chart.
+ */
+export const BarChart = ({ barData }) => {
+  
+  let delayed;
+  return (
+    <Bar
+      data={barData}
+      options={{
+        responsive: true,
+        maintainAspectRatio: false,
+        animation: {
+          onComplete: () => {
+            delayed = true;
+          },
+          delay: (context) => {
+            let delay = 0;
+            if (context.type === 'data' && context.mode === 'default' && !delayed) {
+              delay = context.dataIndex * 150 + context.datasetIndex * 100;
+            }
+            return delay;
+          },
+        }
+      }}
+    />
+  )
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_GeneralStats.js.html b/docs/views_metrics_general-stats_GeneralStats.js.html new file mode 100644 index 0000000..297da8c --- /dev/null +++ b/docs/views_metrics_general-stats_GeneralStats.js.html @@ -0,0 +1,115 @@ + + + + + JSDoc: Source: views/metrics/general-stats/GeneralStats.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/GeneralStats.js

+ + + + + + +
+
+
import React from "react";
+import { JobSeekers } from "./JobSeekers/JobSeekers";
+import { Hours } from "./Hours/Hours";
+import { Shifts } from "./Shifts/Shifts";
+
+/**
+ * @function
+ * @description Creates a page with 3 tabs that show metrics about Shifts, Punctuality, and Hours.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires Hours
+ * @requires Shifts
+ * @requires JobSeekers
+ * @param {object} props - Contains an array of all shifts, and an array of all the workers.
+ */
+export const GeneralStats = (props) => {
+
+    // Setting up main data sources
+    let workers = props.workers
+    let shifts = props.shifts
+    
+    // Return ----------------------------------------------------------------------------------------------------
+
+    return (
+        <>
+          <div className="row d-flex flex-column mx-2">
+                    {/* Tabs Controller Starts */}
+                    <nav>
+                        <div className="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
+                            <a className="nav-item nav-link active" id="nav-shifts-tab" data-toggle="tab" href="#nav-shifts" role="tab" aria-controls="nav-shifts" aria-selected="true"><h2>Shifts</h2></a>
+                            <a className="nav-item nav-link" id="nav-hours-tab" data-toggle="tab" href="#nav-hours" role="tab" aria-controls="nav-hours" aria-selected="false"><h2>Hours</h2></a>
+                            <a className="nav-item nav-link" id="nav-job-seekers-tab" data-toggle="tab" href="#nav-job-seekers" role="tab" aria-controls="nav-job-seekers" aria-selected="false"><h2>Job Seekers</h2></a>
+                        </div>
+                    </nav>
+                    {/* Tabs Controller Ends */}
+
+                    {/* Tabs Content Starts */}
+                    <div
+                        className="tab-content mt-5"
+                        id="nav-tabContent"
+                    >
+                        {/* Shifts Tab Starts */}
+                        <div className="tab-pane fade show active" id="nav-shifts" role="tabpanel" aria-labelledby="nav-shifts-tab">
+                            <Shifts shifts={shifts} />
+                        </div>
+                        {/* Shifts Tab Ends */}
+
+                        {/* Hours Tab Starts */}
+                        <div className="tab-pane fade" id="nav-hours" role="tabpanel" aria-labelledby="nav-hours-tab">
+                            <Hours shifts={shifts} />
+                        </div>
+                        {/* Hours Tab Ends */}
+
+                        {/* Job Seekers Tab Starts */}
+                        <div className="tab-pane fade" id="nav-job-seekers" role="tabpanel" aria-labelledby="nav-job-seekers-tab">
+                            <JobSeekers shifts={shifts} workers={workers} />
+                        </div>
+                        {/* Job Seekers Tab Ends */}
+                    </div>
+                    {/* Tabs Content Ends */}
+                </div>
+        </>
+    )
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_Hours_Hours.js.html b/docs/views_metrics_general-stats_Hours_Hours.js.html new file mode 100644 index 0000000..b18292f --- /dev/null +++ b/docs/views_metrics_general-stats_Hours_Hours.js.html @@ -0,0 +1,154 @@ + + + + + JSDoc: Source: views/metrics/general-stats/Hours/Hours.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/Hours/Hours.js

+ + + + + + +
+
+
import React from "react";
+import { PieChart } from "../../charts";
+import { HoursDataGenerator } from "./HoursData";
+
+
+/**
+ * @function
+ * @description Creates a page with a table and a graph of the hours worked and their trends.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires PieChart
+ * @requires HoursData
+ */
+export const Hours = (props) => {
+
+    // Setting up main data source
+    let HoursData = HoursDataGenerator(props.shifts)
+
+    // Data for pie chart -------------------------------------------------------------------------------------
+
+    // Colors
+    const purple = "#5c00b8";
+    const lightTeal = "#00ebeb";
+    const darkTeal = "#009e9e";
+    const lightPink = "#eb00eb";
+    const darkPink = "#b200b2";
+
+    // Preparing data to be passed to the chart component
+    const hoursData = {
+        labels: HoursData.map((data) => data.description),
+        datasets: [{
+            label: "Hours",
+            data: HoursData.map((data) => data.qty),
+            backgroundColor: [
+                purple, darkPink, lightPink, lightTeal, darkTeal
+            ],
+        }]
+    }
+
+    // Return ----------------------------------------------------------------------------------------------------
+
+    return (
+        <div className="row d-flex d-inline-flex justify-content-between w-100">
+            {/* Left Column Starts */}
+            <div className="col">
+                <div className="row d-flex flex-column justify-content-between">
+                    {/* Hours Table Starts */}
+                    <div className="col text-center">
+                        <h2 className="mb-4">Hours Table</h2>
+
+                        <table className="table table-bordered border-dark text-center">
+                            <thead className="thead-dark">
+                                {/* Table columns */}
+                                <tr>
+                                    <th scope="col"><h3 className="m-0">Description</h3></th>
+                                    <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                    <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                </tr>
+                            </thead>
+
+                            <tbody>
+                                {/* Mapping the data to diplay it as table rows */}
+                                {HoursData.map((item, i) => {
+                                    return item.description === "Available Hours" ? (
+                                        <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    ) : (
+                                        <tr key={i}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    )
+                                })}
+                            </tbody>
+                        </table>
+                    </div>
+                    {/* Hours Table Ends */}
+                </div>
+            </div>
+            {/* Left Column Ends */}
+
+            {/* Right Column Starts */}
+            <div className="col">
+                <div className="row">
+                    {/* Hours Chart Starts*/}
+                    <div className="col text-center">
+                        <h2 className="mb-3">Hours Chart</h2>
+
+                        <div style={{ height: '16rem' }} className="mx-auto">
+                            <PieChart pieData={hoursData} />
+                        </div>
+                    </div>
+                    {/* Hours Chart Ends*/}
+                </div>
+            </div>
+            {/* Right Column Ends */}
+        </div>
+    )
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_Hours_HoursData.js.html b/docs/views_metrics_general-stats_Hours_HoursData.js.html new file mode 100644 index 0000000..f8b1b28 --- /dev/null +++ b/docs/views_metrics_general-stats_Hours_HoursData.js.html @@ -0,0 +1,410 @@ + + + + + JSDoc: Source: views/metrics/general-stats/Hours/HoursData.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/Hours/HoursData.js

+ + + + + + +
+
+
import moment from "moment";
+
+/**
+ * @function
+ * @description Takes in list a of shifts and generates data of all the hours trends for Hours.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @param {object} props - Contains an array of all the shifts.
+ */
+export const HoursDataGenerator = (props) => {
+
+  // Assigning props to variable
+  let shifts = props
+  
+  // 1st - Separation of shifts ----------------------------------------------------------
+
+  // Array for shifts with multiple clock-ins
+  let multipleClockIns = [];
+
+  // Array for single clock-ins made by single workers
+  let singleClockInSingleWorker = [];
+
+  // Gathering both clock-ins and clock-outs in a formatted 
+  // way to keep at the useful data handy at all times.
+  shifts.forEach((shift) => {
+    if (shift.clockin.length > 1) {
+      multipleClockIns.push({
+        starting_at: shift.starting_at,
+        ending_at: shift.ending_at,
+        clockin: shift.clockin,
+        id: shift.id,
+        employees: shift.employees
+      });
+    } else if (shift.clockin.length === 1) {
+      shift.clockin.forEach((clockIn) => {
+        singleClockInSingleWorker.push({
+          id: shift.id,
+          starting_at: shift.starting_at,
+          ending_at: shift.ending_at,
+          started_at: clockIn.started_at,
+          ended_at: clockIn.ended_at
+        });
+      });
+    }
+  });
+
+  // Setting up arrays for shifts with multiple
+  // clock-ins but different amount of workers
+  let multipleClockInsMultipleWorkers = [];
+  let multipleClockInsSingleWorker = [];
+
+  // Separating shifts based on the number of workers present
+  multipleClockIns.forEach((shift) => {
+    if (shift.employees.length > 1) {
+      // Adding shifts to 'multipleClockInsMultipleWorkers'
+      multipleClockInsMultipleWorkers.push(shift.clockin);
+    } else if (shift.employees.length === 1) {
+      // Adding shifts to 'multipleClockInsSingleWorker'
+      multipleClockInsSingleWorker.push(shift.clockin);
+    }
+  });
+
+  // Array of multiple clock-ins with multiple workers, but organized
+  let MCIMWOrganized = [];
+
+  // Adding shifts to 'MCIMWOrganized'
+  multipleClockInsMultipleWorkers.forEach((shift) => {
+    // Unifying shifts that have the same worker
+    let newObj = shift.reduce((obj, value) => {
+      let key = value.employee;
+      if (obj[key] == null) obj[key] = [];
+
+      obj[key].push(value);
+      return obj;
+    }, []);
+
+    newObj.forEach((shift) => {
+      MCIMWOrganized.push(shift);
+    });
+  });
+
+  // Array for the polished version of 'multipleClockInsMultipleWorkers'
+  let MCIMWPolished = [];
+
+  // Array for single clock-ins made by single workers
+  // inside shifts with multiple workers present
+  let singleClockinsMultipleWorkers = [];
+
+  // Separating shifts of multiple workers based on
+  // how many clock-ins each worker has
+  MCIMWOrganized.forEach((shift) => {
+    if (shift.length === 1) {
+      shift.forEach((clockIn) => {
+        singleClockinsMultipleWorkers.push(clockIn);
+      });
+    } else if (shift.length > 1) {
+      MCIMWPolished.push(shift);
+    }
+  });
+
+  // Array for polished version of 'singleClockinsMultipleWorkers'
+  let SCIMWPolished = [];
+
+  // Adding shifts to 'SCIMWPolished' in a formatted 
+  // way to keep at the useful data handy at all times.
+  shifts.forEach((originalShift) => {
+    singleClockinsMultipleWorkers.forEach((filteredShift) => {
+      if (originalShift.id === filteredShift.shift) {
+        SCIMWPolished.push({
+          id: originalShift.id,
+          started_at: filteredShift.started_at,
+          ended_at: filteredShift.ended_at,
+          starting_at: originalShift.starting_at,
+          ending_at: originalShift.ending_at
+        });
+      }
+    });
+  });
+
+  // Combining all shifts with single clock-ins. These will not have break times.
+  let singleClockInsCombined = [...singleClockInSingleWorker, ...SCIMWPolished];
+
+  // Combining all shifts with multiple clock-ins. These will have break times.
+  let multipleClockInsCombined = [
+    ...multipleClockInsSingleWorker,
+    ...MCIMWPolished
+  ];
+
+  // 2nd - Calculation of Hours and Minutes ----------------------------------------------
+
+  // Calculating scheduled hours of all single clock-in shifts ---------------------------
+
+  let SCICScheduledHours = singleClockInsCombined.reduce(
+    (total, { starting_at, ending_at }) =>
+      total +
+      moment.duration(moment(ending_at).diff(moment(starting_at))).asHours(),
+    0
+  );
+
+  // Total scheduled hours
+  let SCICScheduledHoursF = parseInt(
+    (Math.round(SCICScheduledHours * 4) / 4).toFixed(0),
+    10
+  );
+
+  // Calculating worked hours of all single clock-in shifts ------------------------------
+
+  let SCICWorkedHours = singleClockInsCombined.reduce(
+    (total, { started_at, ended_at }) =>
+      total +
+      moment.duration(moment(ended_at).diff(moment(started_at))).asHours(),
+    0
+  );
+
+  // Total worked hours
+  let SCICWorkedHoursF = parseInt(
+    (Math.round(SCICWorkedHours * 4) / 4).toFixed(0),
+    10
+  );
+
+  // Extra worked hours
+  let extraWorkedHoursSingleClockIns = SCICWorkedHoursF - SCICScheduledHoursF;
+
+  // Calculating scheduled hours of all multiple clock-in shifts -------------------------
+
+  // Array for scheduled minutes
+  let MCICScheduledMinutes = [];
+
+  // Adding shifts to 'MCICScheduledMinutes'
+  multipleClockInsCombined.forEach((shift) => {
+    let shiftStart = moment(shift[0].started_at);
+    let shiftEnd = moment(shift[shift.length - 1].ended_at);
+    let id = shift[0].shift;
+    let diff = moment.duration(shiftEnd.diff(shiftStart)).asMinutes();
+
+    MCICScheduledMinutes.push({
+      id: id,
+      employee: shift[0].employee,
+      scheduled_mins: diff
+    });
+  });
+
+  // Total scheduled minutes
+  let TotalMCICScheduledMinutes = MCICScheduledMinutes.reduce((acc, obj) => {
+    return acc + obj.scheduled_mins;
+  }, 0);
+
+  // Total scheduled hours
+  let MCICScheduledHours =
+    Math.floor(TotalMCICScheduledMinutes / 60) + SCICScheduledHoursF;
+
+  // Calculating worked hours of all multiple clock-in shifts ----------------------------
+
+  // Array for worked minutes
+  let MCICWorkedMinutes = [];
+
+  // Adding shifts to 'MCICWorkedMinutes'
+  multipleClockInsCombined.forEach((shift) => {
+    shift.forEach((clockIn) => {
+      let start = moment(clockIn.started_at);
+      let end = moment(clockIn.ended_at);
+      let id = clockIn.shift;
+
+      let diff = moment.duration(end.diff(start)).asMinutes();
+
+      MCICWorkedMinutes.push({
+        id: id,
+        employee: clockIn.employee,
+        worked_mins: diff
+      });
+    });
+  });
+
+  // Polished version of 'MCICWorkedMinutes'
+  let MCICWorkedMinutesPolished = MCICWorkedMinutes.reduce(
+    (result, { id, employee, worked_mins }) => {
+      let temp = result.find((o) => {
+        return o.id === id && o.employee === employee;
+      });
+      if (!temp) {
+        result.push({ id, employee, worked_mins });
+      } else {
+        temp.worked_mins += worked_mins;
+      }
+      return result;
+    },
+    []
+  );
+
+  // Total worked minutes
+  let TotalMCICWorkedMinutes = MCICWorkedMinutesPolished.reduce((acc, obj) => {
+    return acc + obj.worked_mins;
+  }, 0);
+
+  // Total worked hours
+  let MCICWorkedHours =
+    Math.floor(TotalMCICWorkedMinutes / 60) + SCICWorkedHoursF;
+
+  // Extra worked hours
+  let extraWorkedHoursMultipleClockIns = MCICWorkedHours - MCICScheduledHours;
+
+  // Extra calculations -----------------------------------------------------------------
+
+  // Array for the break times
+  let breakTimes = [];
+
+  // Calculating break times of every shift
+  MCICScheduledMinutes.forEach((scheduledShift) => {
+    MCICWorkedMinutesPolished.forEach((workedShift) => {
+      if (
+        scheduledShift.id === workedShift.id &&
+        scheduledShift.employee === workedShift.employee
+      ) {
+        let scheduled = scheduledShift.scheduled_mins;
+        let worked = workedShift.worked_mins;
+
+        let diff = scheduled - worked;
+
+        breakTimes.push({
+          id: scheduledShift.id,
+          break_mins: diff
+        });
+      }
+    });
+  });
+
+  // Array for long breaks
+  let longBreaks = [];
+
+  // Adding shifts to 'longBreaks'
+  breakTimes.forEach((shift) => {
+    if (shift.break_mins > 30) {
+      longBreaks.push(shift);
+    }
+  });
+
+  // Calculating worked hours
+  let workedHours = MCICWorkedHours + SCICWorkedHoursF;
+
+  // Calculating scheduled hours
+  let scheduledHours = MCICScheduledHours + SCICScheduledHoursF;
+
+  // Calculating extra worked hours
+  let extraWorkedHours =
+    extraWorkedHoursSingleClockIns + extraWorkedHoursMultipleClockIns;
+
+  // Setting up conditional rendering of extra worked hours
+  let qtyOfExtraWorkedHours = () => {
+    if (extraWorkedHours > 0) {
+      return extraWorkedHours;
+    } else {
+      return 0;
+    }
+  };
+
+  // 3rd - Setting up objects -----------------------------------------------------------
+
+  // THIS IS A PLACEHOLDER, this number should be
+  // the total hours available of all employees
+  let availableHours = 300;
+
+  // Creating object for scheduled hours
+  let scheduledHoursObj = {
+    description: "Scheduled Hours",
+    qty: scheduledHours
+  };
+
+  // Creating object for worked hours
+  let workedHoursObj = {
+    description: "Worked Hours",
+    qty: workedHours
+  };
+
+  // Creating object for extra worked hours
+  let extraWorkedHoursObj = {
+    description: "Extra Worked Hours",
+    qty: qtyOfExtraWorkedHours()
+  };
+
+  // Creating object for long breaks
+  let longBreaksObj = {
+    description: "Long Breaks",
+    qty: `${longBreaks.length}`
+  };
+
+  // Generate semi-final list
+  let semiFinalList = [];
+
+  // Adding objects to semi-final list
+  semiFinalList.push(scheduledHoursObj);
+  semiFinalList.push(workedHoursObj);
+  semiFinalList.push(extraWorkedHoursObj);
+  semiFinalList.push(longBreaksObj);
+
+  // Generating final array with percentages as new properties
+  let finalList = semiFinalList.map(({ description, qty }) => ({
+    description,
+    qty,
+    pct: ((qty * 100) / availableHours).toFixed(0)
+  }));
+
+  // Generating object of available hours
+  let availableHoursObj = {
+    description: "Available Hours",
+    qty: availableHours,
+    pct: "100"
+  };
+
+  // Adding object of available hours to final list
+  finalList.push(availableHoursObj);
+
+  // Adding IDs to each object in the array
+  finalList.forEach((item, i) => {
+    item.id = i + 1;
+  });
+
+  // Returning the final array
+  return finalList;
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_JobSeekers_JobSeekers.js.html b/docs/views_metrics_general-stats_JobSeekers_JobSeekers.js.html new file mode 100644 index 0000000..97f2153 --- /dev/null +++ b/docs/views_metrics_general-stats_JobSeekers_JobSeekers.js.html @@ -0,0 +1,258 @@ + + + + + JSDoc: Source: views/metrics/general-stats/JobSeekers/JobSeekers.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/JobSeekers/JobSeekers.js

+ + + + + + +
+
+
import React, { useEffect, useState } from "react";
+import { PieChart, BarChart } from '../../charts';
+import { JobSeekersDataGenerator, NewJobSeekersDataGenerator } from "./JobSeekersData";
+
+/**
+ * @function
+ * @description Creates a page with 2 graphs and 2 charts showing trends on active, inactive, and new job seekers.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires PieChart
+ * @requires BarChart
+ * @requires NewJobSeekersDataGenerator
+ * @requires JobSeekersDataGenerator
+ * @param {object} props - Contains an array of all the shifts, and also an array of all the workers.
+ */
+export const JobSeekers = (props) => {
+
+    // Use state to hold list of workers and list of shifts
+    const [workersList, setWorkersList] = useState([])
+    const [shifsList, setShiftsList] = useState([])
+
+    // Receiving the props that contain the lists we need
+    const handleProps = async () => {
+
+        // Catching the props when they arrive
+        let propsObj = await props
+
+        // Checking length of lists before save them
+        if (propsObj.workers.length > 0) {
+            // Saving list of workers
+            setWorkersList(propsObj.workers)
+            // Saving list of shifts
+            setShiftsList(propsObj.shifts)
+        } else {
+            // Handling error with a message
+            console.log("Waiting for props to arrive")
+        }
+    }
+
+    // Triggering handleProps when props change/arrive
+    useEffect(() => {
+        handleProps()
+    }, [props])
+
+    if (workersList.length > 0) {
+
+        // Setting up main data sources
+        let JobSeekersData = JobSeekersDataGenerator(shifsList, workersList)
+        let NewJobSeekersData = NewJobSeekersDataGenerator(workersList)
+
+        // Data for pie chart -------------------------------------------------------------------------------------
+
+        // Taking out the "Totals" from the chart view
+        let pieData = JobSeekersData.filter((item) => { return item.description !== "Total Job Seekers" }) // Taking out the "Totals" from the chart view
+
+        // Preparing data to be passed to the chart component
+        const jobSeekersData = {
+            labels: pieData.map((data) => data.description),
+            datasets: [{
+                label: "Job Seekers",
+                data: pieData.map((data) => data.qty),
+                backgroundColor: [
+                    purple, lightPink
+                ],
+            }]
+        }
+
+        // Data for bar chart -------------------------------------------------------------------------------------
+
+        // Colors
+        const purple = "#5c00b8";
+        const lightPink = "#eb00eb";
+        const darkTeal = "#009e9e";
+        const green = "#06ff05";
+
+        // Preparing data to be passed to the chart component
+        const newJobSeekersData = {
+            labels: NewJobSeekersData.map((data) => data.description),
+            datasets: [{
+                label: "New Job Seekers",
+                data: NewJobSeekersData.map((data) => data.qty),
+                backgroundColor: [
+                    darkTeal, green
+                ],
+            }]
+        }
+
+        // Return ----------------------------------------------------------------------------------------------------
+
+        return (
+            <div className="row d-flex d-inline-flex justify-content-between w-100">
+                {/* Left Column Starts */}
+                <div className="col">
+                    <div className="row d-flex flex-column justify-content-between mb-5">
+                        {/* Job Seekers Table Starts */}
+                        <div className="col text-center">
+                            <h2 className="mb-4">Job Seekers Table</h2>
+
+                            <table className="table table-bordered border-dark text-center">
+                                <thead className="thead-dark">
+                                    {/* Table columns */}
+                                    <tr>
+                                        <th scope="col"><h3 className="m-0">Description</h3></th>
+                                        <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                        <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                    </tr>
+                                </thead>
+
+                                <tbody>
+                                    {/* Mapping the data to diplay it as table rows */}
+                                    {JobSeekersData.map((item, i) => {
+                                        return item.description === "Total Job Seekers" ? (
+                                            <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                                <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                                <td><h3 className="m-0">{item.qty}</h3></td>
+                                                <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                            </tr>
+                                        ) :
+                                            (
+                                                <tr key={i}>
+                                                    <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                                    <td><h3 className="m-0">{item.qty}</h3></td>
+                                                    <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                                </tr>
+                                            )
+                                    })}
+                                </tbody>
+                            </table>
+                        </div>
+                        {/* Job Seekers Table Ends */}
+                    </div>
+
+                    <div className="row d-flex flex-column justify-content-between mb-5">
+                        {/* New Job Seekers Table Starts */}
+                        <div className="col text-center">
+                            <h2 className="mb-4">New Job Seekers Table</h2>
+
+                            <table className="table table-bordered border-dark text-center">
+                                <thead className="thead-dark">
+                                    {/* Table columns */}
+                                    <tr>
+                                        <th scope="col"><h3 className="m-0">Description</h3></th>
+                                        <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                        <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                    </tr>
+                                </thead>
+
+                                <tbody>
+                                    {/* Mapping the data to diplay it as table rows */}
+                                    {NewJobSeekersData.map((item, i) => {
+                                        return item.description === "Total Job Seekers" ? (
+                                            <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                                <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                                <td><h3 className="m-0">{item.qty}</h3></td>
+                                                <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                            </tr>
+                                        ) :
+                                            (
+                                                <tr key={i}>
+                                                    <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                                    <td><h3 className="m-0">{item.qty}</h3></td>
+                                                    <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                                </tr>
+                                            )
+                                    })}
+                                </tbody>
+                            </table>
+                        </div>
+                        {/* New Job Seekers Table Ends */}
+                    </div>
+                </div>
+                {/* Left Column Ends */}
+
+                {/* Right Column Starts */}
+                <div className="col">
+                    <div className="row">
+                        {/* Job Seekers Chart Starts*/}
+                        <div className="col text-center mb-5">
+                            <h2 className="mb-3">Job Seekers Chart</h2>
+
+                            <div style={{ height: '13.90rem' }} className="mx-auto">
+                                <PieChart pieData={jobSeekersData} />
+                            </div>
+                        </div>
+                        {/* Job Seekers Chart Ends*/}
+                    </div>
+
+                    <div className="row">
+                        {/* New Job Seekers Chart Starts*/}
+                        <div className="col text-center">
+                            <h2 className="mb-3">New Job Seekers Chart</h2>
+
+                            <div style={{ height: '13.90rem' }} className="mx-auto">
+                                <BarChart barData={newJobSeekersData} />
+                            </div>
+                        </div>
+                        {/* New Job Seekers Chart Ends*/}
+                    </div>
+                </div>
+                {/* Right Column Ends */}
+            </div>
+        )
+    } else {
+        return (
+            <h1>Loading</h1>
+        )
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_JobSeekers_JobSeekersData.js.html b/docs/views_metrics_general-stats_JobSeekers_JobSeekersData.js.html new file mode 100644 index 0000000..ef971bd --- /dev/null +++ b/docs/views_metrics_general-stats_JobSeekers_JobSeekersData.js.html @@ -0,0 +1,225 @@ + + + + + JSDoc: Source: views/metrics/general-stats/JobSeekers/JobSeekersData.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/JobSeekers/JobSeekersData.js

+ + + + + + +
+
+
import moment from "moment";
+
+// Today
+const now = moment().format("YYYY-MM-DD")
+
+// Today, four weeks in the past
+const fourWeeksBack = moment().subtract(4, 'weeks').format("YYYY-MM-DD")
+
+// Job Seekers Data ------------------------------------------------------------------
+
+/**
+ * @function
+ * @description Takes in list a of shifts and job seekers and generates data of inactive/active job seekers for JobSeekers.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @param {object} props - Contains an array of all the shifts, and also an array of all the workers.
+ */
+export const JobSeekersDataGenerator = (shiftsProp, workersProp) => {
+
+    // Assigning props to variables
+    let shifts = shiftsProp
+    let workers = workersProp
+
+    // Array for all clock-ins
+    let clockInsList = []
+
+    // Gathering the clock-ins of all the shifts
+    shifts.forEach((shift) => {
+        shift.clockin.forEach((clockIn) => {
+            // Keeping all clockins array with at
+            // least one object inside
+            if (shift.clockin.length > 0) {
+                clockInsList.push(clockIn);
+            }
+        })
+    })
+
+    // Array for all recent clock-ins
+    let recentClockIns = []
+
+    // Filtering out clock-ins that happened longer than 4 weeks ago
+    clockInsList.forEach((clockIn) => {
+        let clockInStart = moment(clockIn.started_at).format("YYYY-MM-DD");
+
+        if (clockInStart > fourWeeksBack && clockInStart < now) {
+            recentClockIns.push(clockIn)
+        }
+    })
+
+    // Array for worker ids
+    let workerIDs = []
+
+    // Gethering all worker ids from recent clock-ins
+    recentClockIns.forEach((clockIn) => {
+        workerIDs.push(clockIn.employee)
+    })
+
+    // Filtering out repeated worker ids
+    let filteredWorkerIDs = [...new Set(workerIDs)];
+
+    // Calculating total, active, and inactive workers
+    let totalWorkers = workers.length
+    let totalActiveWorkers = filteredWorkerIDs.length
+    let totalInactiveWorkers = totalWorkers - totalActiveWorkers
+
+    // Setting up objects for the semi-final array
+    let activeWorkers = {
+        id: 1,
+        description: "Active Job Seekers",
+        qty: totalActiveWorkers
+    }
+    let inactiveWorkers = {
+        id: 2,
+        description: "Inactive Job Seekers",
+        qty: totalInactiveWorkers
+    }
+
+    // Creating the semi-final array
+    let semiFinalList = []
+
+    // Adding objects to the semi-final array
+    semiFinalList.push(activeWorkers)
+    semiFinalList.push(inactiveWorkers)
+
+    // Generating final array with percentages as new properties
+    let finalList = semiFinalList.map(({ id, description, qty }) => ({
+        id,
+        description,
+        qty,
+        pct: ((qty * 100) / totalWorkers).toFixed(0)
+    }));
+
+    // Generating the object of total workers
+    let totalJobSeekers = {
+        id: 3,
+        description: "Total Job Seekers",
+        qty: totalWorkers,
+        pct: "100"
+    }
+
+    // Adding the object of total workers to the final array
+    finalList.push(totalJobSeekers)
+
+    // Returning the final array
+    return finalList
+}
+
+// New Job Seekers Data ------------------------------------------------------------------
+
+/**
+ * @function
+ * @description Takes in list a of job seekers and generates data of new job seekers for JobSeekers.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @param {object} props - Contains an array of all the shifts, and also an array of all the workers.
+ */
+export const NewJobSeekersDataGenerator = (props) => {
+
+    // Assigning props to variable
+    // Here we only need the array of workers
+    let workers = props
+
+    // Array for new workers
+    let newWorkersList = []
+
+    // Adding workers to 'newWorkersList' based on their creation date
+    workers.forEach((worker) => {
+        let creation_date = moment(worker.created_at).format("YYYY-MM-DD");
+
+        if (creation_date > fourWeeksBack && creation_date < now) {
+            newWorkersList.push(worker)
+        }
+    })
+
+    // Setting up some variables for the objects
+    let totalWorkers = workers.length
+    let totalNewWorkers = newWorkersList.length
+
+    // Setting up objects for the semi-final array
+    let newWorkers = {
+        id: 0,
+        description: "New Job Seekers",
+        qty: totalNewWorkers
+    }
+
+    // Creating the semi-final array
+    let semiFinalList = []
+
+    // Adding objects to the semi-final array
+    semiFinalList.push(newWorkers)
+
+    // Generating final array with percentages as new properties
+    let finalList = semiFinalList.map(({ id, description, qty }) => ({
+        id,
+        description,
+        qty,
+        pct: ((qty * 100) / totalWorkers).toFixed(0)
+    }));
+
+    // Generating the object of total workers
+    let totalJobSeekers = {
+        id: 1,
+        description: "Total Job Seekers",
+        qty: totalWorkers,
+        pct: "100"
+    }
+
+    // Adding the object of total workers to the final array
+    finalList.push(totalJobSeekers)
+
+    // Returning the final array
+    return finalList
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_Shifts_Shifts.js.html b/docs/views_metrics_general-stats_Shifts_Shifts.js.html new file mode 100644 index 0000000..c4ebadb --- /dev/null +++ b/docs/views_metrics_general-stats_Shifts_Shifts.js.html @@ -0,0 +1,157 @@ + + + + + JSDoc: Source: views/metrics/general-stats/Shifts/Shifts.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/Shifts/Shifts.js

+ + + + + + +
+
+
import React from "react";
+import { BarChart } from "../../charts";
+import { ShiftsDataGenerator } from "./ShiftsData";
+
+/**
+ * @function
+ * @description Creates a page with a table and a graph of all the shift statuses.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires ShiftsDataGenerator
+ * @requires BarChart
+ * @param {object} props - Contains an array of all the shifts.
+ */
+export const Shifts = (props) => {
+
+    // Setting up main data source
+    let ShiftsData = ShiftsDataGenerator(props.shifts)
+
+    // Data for bar chart -------------------------------------------------------------------------------------
+
+    // Colors
+    const purple = "#5c00b8";
+    const lightTeal = "#00ebeb";
+    const darkTeal = "#009e9e";
+    const lightPink = "#eb00eb";
+    const darkPink = "#b200b2";
+
+    // Taking out the "Totals" from the chart view
+    let barData = ShiftsData.filter((item) => { return item.description !== "Total Shifts Posted " }) // Taking out the "Totals" from the chart view
+
+    // Preparing data to be passed to the chart component
+    const shiftsData = {
+        labels: barData.map((data) => data.description),
+        datasets: [{
+            label: "Shifts",
+            data: barData.map((data) => data.qty),
+            backgroundColor: [
+                purple, darkPink, lightPink, lightTeal, darkTeal
+            ],
+        }]
+    }
+
+    // Return ----------------------------------------------------------------------------------------------------
+
+    return (
+        <div className="row d-flex d-inline-flex justify-content-between" style={{ width: "100%" }}>
+            {/* Left Column Starts */}
+            <div className="col">
+                <div className="row d-flex flex-column justify-content-between">
+                    {/* Shifts Table Starts */}
+                    <div className="col text-center">
+                        <h2 className="mb-4">Shifts Table</h2>
+
+                        <table className="table table-bordered border-dark text-center">
+                            <thead className="thead-dark">
+                                {/* Table columns */}
+                                <tr>
+                                    <th scope="col"><h3 className="m-0">Description</h3></th>
+                                    <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                    <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                </tr>
+                            </thead>
+
+                            <tbody>
+                                {/* Mapping the data to diplay it as table rows */}
+                                {ShiftsData.map((item, i) => {
+                                    return item.description === "Total Shifts Posted" ? (
+                                        <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    ) : (
+                                        <tr key={i}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    )
+                                })}
+                            </tbody>
+                        </table>
+                    </div>
+                    {/* Shifts Table Ends */}
+                </div>
+            </div>
+            {/* Left Column Ends */}
+
+            {/* Right Column Starts */}
+            <div className="col">
+                <div className="row">
+                    {/* Shifts Chart Starts*/}
+                    <div className="col text-center">
+                        <h2 className="mb-3">Shifts Chart</h2>
+
+                        <div style={{ height: '20rem' }}>
+                            <BarChart barData={shiftsData} />
+                        </div>
+                    </div>
+                    {/* Shifts Chart Ends*/}
+                </div>
+            </div>
+            {/* Right Column Ends */}
+        </div>
+    )
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_general-stats_Shifts_ShiftsData.js.html b/docs/views_metrics_general-stats_Shifts_ShiftsData.js.html new file mode 100644 index 0000000..8748d5b --- /dev/null +++ b/docs/views_metrics_general-stats_Shifts_ShiftsData.js.html @@ -0,0 +1,146 @@ + + + + + JSDoc: Source: views/metrics/general-stats/Shifts/ShiftsData.js + + + + + + + + + + +
+ +

Source: views/metrics/general-stats/Shifts/ShiftsData.js

+ + + + + + +
+
+
/**
+ * @function
+ * @description Takes in list a of shifts and generates data of shift statuses for Shifts.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @param {object} props - Contains a list of all shifts.
+ */
+export const ShiftsDataGenerator = (props) => {
+
+    // Assigning props to variable
+    let shifts = props
+
+    // First array
+    let shiftsList = [];
+
+    // Gathering all the existing shifts
+    shifts.forEach((shift) => {
+        shiftsList.push({
+            status: shift.status,
+            clockin: shift.clockin,
+            employees: shift.employees
+        });
+    });
+
+    // Setting up counters
+    let open = 0
+    let filled = 0
+    let completed = 0
+    let rejected = 0
+    let total = shiftsList.length
+
+    // Adding values to each counter based on certain shift conditions
+    shiftsList.forEach((item) => {
+        if (item.status === "EXPIRED" && item.clockin.length === 0 && item.employees.length === 0) {
+            rejected++
+        } else if (item.status === "FILLED") {
+            filled++
+        } else if (item.status === "COMPLETED") {
+            completed++
+        } else if (item.status === "OPEN") {
+            open++
+        }
+    })
+
+    // Creating shift objects
+    let openShifts = {
+        description: "Open Shifts",
+        qty: open
+    }
+    let filledShifts = {
+        description: "Filled Shifts",
+        qty: filled
+    }
+    let workedShifts = {
+        description: "Worked Shifts",
+        qty: completed
+    }
+    let rejectedShifts = {
+        description: "Rejected Shifts",
+        qty: rejected
+    }
+
+    // Setting up base array for all shift objects
+    let cleanedArray = []
+
+    // Pushing shift objects to base array
+    cleanedArray.push(openShifts)
+    cleanedArray.push(filledShifts)
+    cleanedArray.push(workedShifts)
+    cleanedArray.push(rejectedShifts)
+
+    // Generating final array with percentages as new properties
+    let percentagesArray = cleanedArray.map(({ description, qty }) => ({
+        description,
+        qty,
+        pct: ((qty * 100) / total).toFixed(0)
+    }));
+
+    // Generating the object of total shifts
+    let totalShifs = {
+        description: "Total Shifts Posted",
+        qty: total,
+        pct: "100"
+    }
+
+    // Adding the object of total shifts to the final array
+    percentagesArray.push(totalShifs)
+
+    // Adding id's to each object in the final array
+    percentagesArray.forEach((item, i) => {
+        item.id = i + 1;
+    });
+
+    // Returning the final array
+    return percentagesArray
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_metrics.js.html b/docs/views_metrics_metrics.js.html new file mode 100644 index 0000000..42debcb --- /dev/null +++ b/docs/views_metrics_metrics.js.html @@ -0,0 +1,228 @@ + + + + + JSDoc: Source: views/metrics/metrics.js + + + + + + + + + + +
+ +

Source: views/metrics/metrics.js

+ + + + + + +
+
+
import React from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import { Session } from "bc-react-session";
+
+import { Queue } from "./queue/Queue";
+import { Punctuality } from "./punctuality/Punctuality";
+import { Ratings } from "./ratings/Ratings";
+import { GeneralStats } from "./general-stats/GeneralStats";
+
+import { store, search } from "../../actions";
+
+/**
+ * @description Creates the view for Metrics page, which renders 4 tabs with different components being called inside each one.
+ * @since 09.28.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires Punctuality
+ * @requires Ratings
+ * @requires GeneralStats
+ * @requires Queue
+ * @requires store
+ * @requires search
+ * @requires Session
+ * @requries Flux
+ */
+export class Metrics extends Flux.DashView {
+
+    constructor() {
+        super();
+        this.state = {
+            // Queue Data ----------------------------------------------------------------------------------------------------------------------------
+
+            // Variables for the Workers' List
+            employees: [], // This will hold all the shifts.
+            DocStatus: "", //This is needed to check the verification status of employees.
+            empStatus: "unverified", //This is needed to filter out unverified employees.
+
+            // Variables for the Shifts' List
+            allShifts: [], // This will hold all the shifts.
+            session: Session.get(), // This is needed to crate a user/employer session.
+            calendarLoading: true, // This is needed to fill up the list of shifts, not sure how.
+        };
+
+        // This updates the state values
+        this.handleStatusChange = this.handleStatusChange.bind(this);
+    }
+
+    // Generating the list of shifts and the list of employees ---------------------------------------------------------------------------------------
+
+    componentDidMount() {
+        // Processes for the Shifts' List (Not sure how they work)
+        const shifts = store.getState("shifts");
+
+        this.subscribe(store, "shifts", (_shifts) => {
+            this.setState({ allShifts: _shifts, calendarLoading: false });
+        });
+
+        // Processes for the Workers' List (Not sure how they work)
+        this.filter();
+
+        this.subscribe(store, "employees", (employees) => {
+            if (Array.isArray(employees) && employees.length !== 0)
+                this.setState({ employees });
+        });
+
+        this.handleStatusChange
+    }
+
+    // Processes for the Workers' List (Not sure how they work)
+    componentWillUnmount() {
+        this.handleStatusChange
+    }
+
+    handleStatusChange() {
+        this.setState({ DocStatus: props.catalog.employee.employment_verification_status });
+    }
+
+    filter(url) {
+        let queries = window.location.search;
+
+        if (queries) queries = "&" + queries.substring(1);
+
+        if (url && url.length > 50) {
+            const page = url.split("employees")[1];
+
+            if (page) {
+                search(`employees`, `${page + queries}`).then((data) => {
+                    this.setState({
+                        employees: data.results,
+                    });
+                });
+
+            } else null;
+
+        } else {
+            search(`employees`, `?envelope=true&limit=50${queries}`).then((data) => {
+                this.setState({
+                    employees: data.results,
+                });
+            });
+        }
+    }
+
+    // Render ---------------------------------------------------------------------------------------------------------------------------------------
+
+    render() {
+        // List of workers with verified documents
+        let verifiedEmpList = this.state.employees.filter((employees) => employees.employment_verification_status === "APPROVED")
+
+        // List of all shifts
+        let listOfShifts = this.state.allShifts;
+
+        // ---------------------------------------------
+        // Filtering expired shifts
+        // let listOfShifts =
+        //     (Array.isArray(shifts) &&
+        //         shifts.length > 0 &&
+        //         shifts.filter(
+        //             (e) => e.status !== "EXPIRED"
+        //         )) ||
+        //     [];
+        // ---------------------------------------------
+
+        // Return -----------------------------------------------------------------------------------------------------------------------------------
+
+        return (
+            <div>
+                {/* Title of the Page*/}
+                <div className="mx-3 mb-3">
+                    <h1>Metrics</h1>
+                </div>
+
+                <div className="row d-flex flex-column mx-3">
+                    {/* Tabs Controller Starts */}
+                    <nav>
+                        <div className="nav nav-tabs nav-fill" id="nav-tab" role="tablist">
+                            <a className="nav-item nav-link active" id="nav-general-stats-tab" data-toggle="tab" href="#nav-general-stats" role="tab" aria-controls="nav-general-stats" aria-selected="true"><h2>General Stats</h2></a>
+                            <a className="nav-item nav-link" id="nav-punctuality-tab" data-toggle="tab" href="#nav-punctuality" role="tab" aria-controls="nav-punctuality" aria-selected="false"><h2>Punctuality</h2></a>
+                            <a className="nav-item nav-link" id="nav-ratings-tab" data-toggle="tab" href="#nav-ratings" role="tab" aria-controls="nav-ratings" aria-selected="false"><h2>Ratings</h2></a>
+                            <a className="nav-item nav-link" id="nav-queue-tab" data-toggle="tab" href="#nav-queue" role="tab" aria-controls="nav-queue" aria-selected="false"><h2>Queue</h2></a>
+                        </div>
+                    </nav>
+                    {/* Tabs Controller Ends */}
+
+                    {/* Tabs Content Starts */}
+                    <div
+                        className="tab-content mt-5"
+                        id="nav-tabContent"
+                    >
+                        {/* General Stats Tab Starts */}
+                        <div className="tab-pane fade show active" id="nav-general-stats" role="tabpanel" aria-labelledby="nav-general-stats-tab">
+                            <GeneralStats workers={verifiedEmpList} shifts={listOfShifts} />
+                        </div>
+                        {/* General Stats Tab Ends */}
+
+                        {/* Punctuality Tab Starts */}
+                        <div className="tab-pane fade" id="nav-punctuality" role="tabpanel" aria-labelledby="nav-punctuality-tab">
+                            <Punctuality shifts={listOfShifts} />
+                        </div>
+                        {/* Punctuality Tab Ends */}
+
+                        {/* Ratings Tab Starts */}
+                        <div className="tab-pane fade" id="nav-ratings" role="tabpanel" aria-labelledby="nav-ratings-tab">
+                            <Ratings workers={verifiedEmpList} />
+                        </div>
+                        {/* Ratings Tab Ends */}
+
+                        {/* Queue Tab Starts */}
+                        <div className="tab-pane fade" id="nav-queue" role="tabpanel" aria-labelledby="nav-queue-tab">
+                            <Queue workers={verifiedEmpList} shifts={listOfShifts} />
+                        </div>
+                        {/* Queue Tab Ends */}
+                    </div>
+                    {/* Tabs Content Ends */}
+                </div>
+            </div>
+        );
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_punctuality_Punctuality.js.html b/docs/views_metrics_punctuality_Punctuality.js.html new file mode 100644 index 0000000..843aa70 --- /dev/null +++ b/docs/views_metrics_punctuality_Punctuality.js.html @@ -0,0 +1,233 @@ + + + + + JSDoc: Source: views/metrics/punctuality/Punctuality.js + + + + + + + + + + +
+ +

Source: views/metrics/punctuality/Punctuality.js

+ + + + + + +
+
+
import React from "react";
+import { PieChart } from '../charts';
+import { ClockInsDataGenerator, ClockOutsDataGenerator } from "./PunctualityData";
+
+/**
+ * @function
+ * @description Creates a page with 2 tables and 2 graphs of the clock-in and clock-out trends.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires PieChart
+ * @requires ClockInsDataGenerator
+ * @requires ClockOutsDataGenerator
+ * @param {object} props - Contains an array of all the shifts.
+ */
+export const Punctuality = (props) => {
+
+    // Setting up main data sources
+    let ClockInsData = ClockInsDataGenerator(props.shifts)
+    let ClockOutsData = ClockOutsDataGenerator(props.shifts)
+
+    // Data for pie charts -------------------------------------------------------------------------------------
+
+    // Colors
+    const purple = "#5c00b8";
+    const lightTeal = "#00ebeb";
+    const darkTeal = "#009e9e";
+    const green = "#06ff05";
+    const lightPink = "#eb00eb";
+    const darkPink = "#b200b2";
+
+    // Clock-Ins ------------------------------------------------------------------------------------------------
+
+    // Taking out the "Totals" from the chart view
+    let dataCI = ClockInsData.filter((item) => {
+        return item.description !== "Total Clock-Ins";
+    });
+
+    // Preparing the data to be passed to the chart component
+    const clockInsData = {
+        labels: dataCI.map((data) => data.description),
+        datasets: [
+            {
+                label: "Clock-Ins",
+                data: dataCI.map((data) => data.qty),
+                backgroundColor: [green, lightTeal, darkPink]
+            }
+        ]
+    };
+
+    // Clock-Outs ------------------------------------------------------------------------------------------------
+
+    // Taking out the "Totals" from the chart view
+    let dataCO = ClockOutsData.filter((item) => {
+        return item.description !== "Total Clock-Outs";
+    });
+
+    // Preparing the data to be passed to the chart component
+    const clockOutsData = {
+        labels: dataCO.map((data) => data.description),
+        datasets: [
+            {
+                label: "Clock-Outs",
+                data: dataCO.map((data) => data.qty),
+                backgroundColor: [purple, darkTeal, lightTeal, lightPink]
+            }
+        ]
+    };
+
+    // Return ----------------------------------------------------------------------------------------------------
+
+    return (
+        <div className="row d-flex d-inline-flex justify-content-between w-100">
+            {/* Left Column Starts */}
+            <div className="col">
+                <div className="row d-flex flex-column justify-content-between mb-5">
+                    {/* Clock-Ins Table Starts */}
+                    <div className="col text-center">
+                        <h2 className="mb-4">Clock-Ins Table</h2>
+
+                        <table className="table table-bordered border-dark text-center">
+                            <thead className="thead-dark">
+                                {/* Table columns */}
+                                <tr>
+                                    <th scope="col"><h3 className="m-0">Description</h3></th>
+                                    <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                    <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                </tr>
+                            </thead>
+
+                            <tbody>
+                                {/* Mapping the data to diplay it as table rows */}
+                                {ClockInsData.map((item, i) => {
+                                    return item.description === "Total Clock-Ins" ? (
+                                        <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    ) : (
+                                        <tr key={i}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    )
+                                })}
+                            </tbody>
+                        </table>
+                    </div>
+                    {/* Clock-Ins Table Ends */}
+                </div>
+
+                <div className="row d-flex flex-column justify-content-between">
+                    {/* Clock-Outs Table Starts */}
+                    <div className="col text-center">
+                        <h2 className="mb-4">Clock-Outs Table</h2>
+
+                        <table className="table table-bordered border-dark text-center">
+                            <thead className="thead-dark">
+                                {/* Table columns */}
+                                <tr>
+                                    <th scope="col"><h3 className="m-0">Description</h3></th>
+                                    <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                    <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                </tr>
+                            </thead>
+
+                            <tbody>
+                                {/* Mapping the data to diplay it as table rows */}
+                                {ClockOutsData.map((item, i) => {
+                                    return item.description === "Total Clock-Outs" ? (
+                                        <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    ) : (
+                                        <tr key={i}>
+                                            <th scope="row"><h3 className="m-0">{item.description}</h3></th>
+                                            <td><h3 className="m-0">{item.qty}</h3></td>
+                                            <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                        </tr>
+                                    )
+                                })}
+                            </tbody>
+                        </table>
+                    </div>
+                    {/* Clock-Outs Table Ends */}
+                </div>
+            </div>
+            {/* Left Column Ends */}
+
+            {/* Right Column Starts */}
+            <div className="col">
+                <div className="row mb-5">
+                    {/* Clock-Ins Chart Starts */}
+                    <div className="col text-center mb-5">
+                        <h2 className="mb-3">Clock-Ins Chart</h2>
+
+                        <div style={{ height: '13.905rem' }}>
+                            <PieChart pieData={clockInsData} />
+                        </div>
+                    </div>
+                    {/* Clock-Ins Chart Ends */}
+                </div>
+
+                <div className="row mt-5">
+                    {/* Clock-Outs Chart Starts */}
+                    <div className="col text-center">
+                        <h2 className="mb-3">Clock-Outs Chart</h2>
+
+                        <div style={{ height: '13.905rem' }}>
+                            <PieChart pieData={clockOutsData} />
+                        </div>
+                    </div>
+                    {/* Clock-Outs Chart Ends */}
+                </div>
+            </div>
+            {/* Right Column Ends */}
+        </div>
+    )
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_punctuality_PunctualityData.js.html b/docs/views_metrics_punctuality_PunctualityData.js.html new file mode 100644 index 0000000..57887cd --- /dev/null +++ b/docs/views_metrics_punctuality_PunctualityData.js.html @@ -0,0 +1,282 @@ + + + + + JSDoc: Source: views/metrics/punctuality/PunctualityData.js + + + + + + + + + + +
+ +

Source: views/metrics/punctuality/PunctualityData.js

+ + + + + + +
+
+
import moment from "moment";
+
+// Clock-Ins Data ------------------------------------------------------------------
+
+/**
+ * @function
+ * @description Generates array of objects with clock-in trends for Punctuality.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @param {object} props - Contains an array of all the shifts.
+ */
+export const ClockInsDataGenerator = (props) => {
+
+    // Assigning props to variables
+    let shifts = props
+
+    // Array for the clock-ins
+    let clockIns = [];
+
+    // Sorting the shifts
+    shifts.forEach((shift) => {
+        shift.clockin.forEach((clockIn) => {
+            // Keeping all clockins array with at
+            // least one object inside
+            if (shift.clockin.length > 0) {
+                // Formatting each object to keep
+                // both the scheduled clock-in and 
+                // the actual "registered" clock-in.
+                clockIns.push({
+                    starting_at: shift.starting_at,
+                    started_at: clockIn.started_at
+                });
+            }
+        });
+    });
+
+    // Setting up counters
+    let earlyClockins = 0;
+    let lateClockins = 0;
+    let onTimeClockins = 0;
+
+    // Increasing counters based on clock-in times
+    clockIns.forEach((shift) => {
+        let start1 = moment(shift.starting_at);
+        let start2 = moment(shift.started_at);
+
+        let startDiff = moment.duration(start2.diff(start1)).asMinutes();
+
+        if (startDiff >= 15) {
+            lateClockins++;
+        } else if (startDiff <= -30) {
+            earlyClockins++;
+        } else {
+            onTimeClockins++;
+        }
+    });
+
+    // Creating clock-in objects
+    let earlyClockinsObj = {
+        description: "Early Clock-Ins",
+        qty: earlyClockins
+    };
+    let lateClockinsObj = {
+        description: "Late Clock-Ins",
+        qty: lateClockins
+    };
+    let onTimeClockinsObj = {
+        description: "On Time Clock-Ins",
+        qty: onTimeClockins
+    };
+
+    // Setting up base array for all objects
+    let cleanedClockIns = [];
+
+    // Pushing objects to base array
+    cleanedClockIns.push(earlyClockinsObj);
+    cleanedClockIns.push(lateClockinsObj);
+    cleanedClockIns.push(onTimeClockinsObj);
+
+    // Setting up totals
+    let totalClockIns = clockIns.length;
+
+    // Generating percentages as new properties
+    let pctClockIns = cleanedClockIns.map(({ description, qty }) => ({
+        description,
+        qty,
+        pct: ((qty * 100) / totalClockIns).toFixed(0)
+    }));
+
+    // Setting up object for totals
+    let totalClockInsObj = {
+        description: "Total Clock-Ins",
+        qty: totalClockIns,
+        pct: "100"
+    };
+
+    // Adding totals to the array with percentages
+    pctClockIns.push(totalClockInsObj);
+
+    // Addind IDs to each object
+    pctClockIns.forEach((item, i) => {
+        item.id = i + 1;
+    });
+
+    // Returning clock-ins array
+    return pctClockIns;
+};
+
+// Clock-Outs Data -----------------------------------------------------------------
+
+/**
+ * @function
+ * @description Generates array of objects with clock-out trends for Punctuality.js.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @param {object} props - Contains an array of all the shifts.
+ */
+export const ClockOutsDataGenerator = (props) => {
+
+    // Assigning props to variables
+    let shifts = props
+
+    // Array for the clock-outs
+    let clockOuts = [];
+
+    // Sorting the shifts
+    shifts.forEach((shift) => {
+        shift.clockin.forEach((clockIn) => {
+            // Keeping all clockins array with at
+            // least one object inside
+            if (shift.clockin.length > 0) {
+                // Formatting each object to keep
+                // both the scheduled clock-out, the 
+                // actual "registered" clock-out, and
+                // whether it closed automatically or not.
+                clockOuts.push({
+                    ending_at: shift.ending_at,
+                    ended_at: clockIn.ended_at,
+                    automatically_closed: clockIn.automatically_closed
+                });
+            }
+        });
+    });
+
+    // Setting up counters
+    let earlyClockouts = 0;
+    let lateClockouts = 0;
+    let onTimeClockouts = 0;
+    let forgotClockOut = 0;
+
+    // Increasing counters based on clock-out times
+    clockOuts.forEach((shift) => {
+        let end1 = moment(shift.ending_at);
+        let end2 = moment(shift.ended_at);
+
+        let endDiff = moment.duration(end2.diff(end1)).asMinutes();
+
+        if (endDiff >= 30) {
+            lateClockouts++;
+        } else if (endDiff <= -30) {
+            earlyClockouts++;
+        } else {
+            onTimeClockouts++;
+        }
+    });
+
+    // Increasing the "forgotClockOut" counter only
+    clockOuts.forEach((shift) => {
+        // Note: When a shif get automatically closed, it means
+        // that the worker forgot to clock-out.
+        if (shift.automatically_closed === true) {
+            forgotClockOut++;
+        }
+    });
+
+    // Creating clock-out objects
+    let earlyClockoutsObj = {
+        description: "Early Clock-Outs",
+        qty: earlyClockouts
+    };
+    let lateClockoutsObj = {
+        description: "Late Clock-Outs",
+        qty: lateClockouts
+    };
+    let onTimeClockoutsObj = {
+        description: "On Time Clock-Outs",
+        qty: onTimeClockouts
+    };
+    let forgotClockOutObj = {
+        description: "Forgotten Clock-Outs",
+        qty: forgotClockOut
+    };
+
+    // Setting up base array for all objects
+    let cleanedClockOuts = [];
+
+    // Pushing objects to base array
+    cleanedClockOuts.push(earlyClockoutsObj);
+    cleanedClockOuts.push(lateClockoutsObj);
+    cleanedClockOuts.push(onTimeClockoutsObj);
+    cleanedClockOuts.push(forgotClockOutObj);
+
+    // Setting up totals
+    let totalClockOuts = clockOuts.length;
+
+    // Generating percentages as new properties
+    let pctClockOuts = cleanedClockOuts.map(({ description, qty }) => ({
+        description,
+        qty,
+        pct: ((qty * 100) / totalClockOuts).toFixed(0)
+    }));
+
+    // Setting up object for totals
+    let totalClockOutsObj = {
+        description: "Total Clock-Outs",
+        qty: totalClockOuts,
+        pct: "100"
+    };
+
+    // Adding totals to the array with percentages
+    pctClockOuts.push(totalClockOutsObj);
+
+    // Addind IDs to each object
+    pctClockOuts.forEach((item, i) => {
+        item.id = i + 1;
+    });
+
+    // Returning clock-outs array
+    return pctClockOuts;
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_queue_Queue.js.html b/docs/views_metrics_queue_Queue.js.html new file mode 100644 index 0000000..4b7b683 --- /dev/null +++ b/docs/views_metrics_queue_Queue.js.html @@ -0,0 +1,199 @@ + + + + + JSDoc: Source: views/metrics/queue/Queue.js + + + + + + + + + + +
+ +

Source: views/metrics/queue/Queue.js

+ + + + + + +
+
+
import React, { useState, useEffect } from "react";
+import "react-datepicker/dist/react-datepicker.css";
+import DatePicker from "react-datepicker";
+import moment from "moment";
+
+import { QueueData } from "./QueueData";
+import { Button } from "../../../components/index";
+
+/**
+ * @function
+ * @description Creates a page with a DatePicker and table of all employees with their worked/scheduled hours.
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @requires DatePicker
+ * @requires QueueData
+ * @requires Button
+ * @param {object} props - Contains an array of all shifts, and an array of all workers.
+ */
+export const Queue = (props) => {
+
+  // Setting up my variables ---------------------------------------------------------------------------------
+
+  // Setting up main data source
+  let allShifts = props.shifts;
+  let workers = props.workers;
+
+  // Date selected through the DatePicker
+  const [selectedDate, setSelectedDate] = useState(new Date());
+
+  // Monday of X week (default: current week)
+  const [start, setStart] = useState(
+    moment().startOf("isoWeek").format("YYYY-MM-DD")
+  );
+
+  // Sunday of X week (default: current week)
+  const [end, setEnd] = useState(
+    moment().endOf("isoWeek").format("YYYY-MM-DD")
+  );
+
+  // Function that filters shifts based on the Monday and Sunday of the selected date --------------------------
+
+  const filterShifts = () => {
+
+    // Array for filtered shifts
+    let filteredShifts = [];
+
+    // Keeping shifts that exist within the selected dates
+    allShifts.forEach((shift) => {
+      let shiftStart = moment(shift.starting_at).format("YYYY-MM-DD");
+      let shiftEnd = moment(shift.ending_at).format("YYYY-MM-DD");
+
+      if (
+        shiftStart >= start &&
+        shiftStart <= end &&
+        shiftEnd >= start &&
+        shiftEnd <= end
+      ) {
+        filteredShifts.push(shift);
+      }
+    });
+
+    // Returning filtered shifts
+    return filteredShifts;
+  };
+
+  // UseEffect to update Mondays and Sundays when a new date is selected --------------------------------------
+
+  useEffect(() => {
+
+    // Setting up the new Monday
+    let formattedStart = moment(selectedDate)
+      .startOf("isoWeek")
+      .format("YYYY-MM-DD");
+
+    setStart(formattedStart);
+
+    // Setting up the new Sunday
+    let formattedEnd = moment(selectedDate)
+      .endOf("isoWeek")
+      .format("YYYY-MM-DD");
+
+    setEnd(formattedEnd);
+
+  }, [selectedDate]);
+
+  // Return ----------------------------------------------------------------------------------------------------
+
+  return (
+    <div className="row d-flex flex-column justify-content-center mx-auto w-100">
+      <h2 className="mx-auto">Table of Employee Hours</h2>
+      {/* Top Column Starts */}
+      <div className="col d-flex d-inline-flex justify-content-center my-4 p-3 px-4" style={{ background: "rgba(107, 107, 107, 0.15)" }}>
+        {/* Controls for the Week Starts */}
+        {/* Col 1 */}
+        <div className="col-4 p-0 pt-2">
+          <h3 className="m-0">{`Week of ${start} - ${end}`}</h3>
+        </div>
+
+        {/* Col 2 */}
+        <div className="col-4 p-0 pt-2 d-flex d-inline-flex justify-content-center">
+          <div className="mr-3">
+            <h3 className="m-0">Select a day of the desired week: </h3>
+          </div>
+
+          {/* Calendar/DatePicker */}
+          <div>
+            <DatePicker
+              selected={selectedDate}
+              onChange={(date) => setSelectedDate(date)}
+            />
+          </div>
+        </div>
+
+        {/* Col 3 */}
+        <div className="col-4 p-0 d-flex justify-content-end">
+          <div>
+            <Button className="btn btn-dark bg-dark mr-3" onClick={() => alert("No functionality yet")}>
+              <h6 className="m-0">Placeholder 1</h6>
+            </Button>
+          </div>
+
+          <div>
+            <Button className="btn btn-dark bg-dark" onClick={() => alert("No functionality yet")}>
+              <h6 className="m-0">Placeholder 2</h6>
+            </Button>
+          </div>
+        </div>
+        {/* Controls for the Week Ends */}
+      </div>
+      {/* Top Column Ends */}
+
+      {/* Bottom Column Starts */}
+      {/* Table of Employees Starts */}
+      <div className="col rounded mt-2 m-0 p-0 text-center mx-auto">
+        {workers?.map((singleWorker, i) => {
+          return (
+            <QueueData
+              key={i}
+              worker={singleWorker}
+              shifts={filterShifts()}
+            />)
+        })}
+      </div>
+      {/* Table of Employees Ends */}
+      {/* Bottom Column Ends */}
+    </div>
+  );
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_queue_QueueData.js.html b/docs/views_metrics_queue_QueueData.js.html new file mode 100644 index 0000000..c9f6513 --- /dev/null +++ b/docs/views_metrics_queue_QueueData.js.html @@ -0,0 +1,218 @@ + + + + + JSDoc: Source: views/metrics/queue/QueueData.js + + + + + + + + + + +
+ +

Source: views/metrics/queue/QueueData.js

+ + + + + + +
+
+
import React from "react";
+import moment from "moment";
+import Avatar from "../../../components/avatar/Avatar";
+import { Button, Theme } from "../../../components/index";
+
+// This is needed to render the button "Invite to Shift"
+const allowLevels = window.location.search != "";
+
+/**
+ * @function
+ * @description Creates a table of all employees with their worked/scheduled hours for Queue.js
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires moment
+ * @requires Avatar
+ * @requires Button
+ * @requires Theme
+ * @param {object} props - Contains an array of all shifts, and an object with information of a single worker, previously mapped in Queue.js
+ */
+export const QueueData = (props) => {
+
+    //Assigning props to variables ----------------------------
+
+    // This is a single worker brought from the mapping
+    // of all workers back in Queue.js
+    const worker = props.worker;
+
+    // These are all the shifts, with different workers
+    const shifts = props.shifts;
+
+    // Worker Shifts ------------------------------------------
+
+    // Array to hold shifts from the single worker
+    const workerShifts = [];
+
+    // Keeping the shifts whose worker id matches
+    // the id of the single worker
+    shifts.forEach((shift) => {
+        shift.employees.forEach((employee) => {
+            if (employee === worker.id) {
+                workerShifts.push(shift);
+            }
+        });
+    });
+
+    // Worker Clock-ins ---------------------------------------
+
+    // Array to hold clock-ins from the single worker
+    let workerClockIns = [];
+
+    // Keeping the clock-ins whose worker id matches
+    // the id of the single worker
+    workerShifts.forEach((shift) => {
+        shift.clockin.forEach((clockIn) => {
+            if (clockIn.employee === worker.id) {
+                workerClockIns.push(clockIn);
+            }
+        });
+    });
+
+    // Scheduled Hours ---------------------------------------
+
+    // Array to hold scheduled hours from the single worker
+    let scheduledHours = [];
+
+    // Calculating the scheduled hours of each shift and
+    // passing that data as an object to "scheduledHours"
+    workerShifts.forEach((shift) => {
+        let start = moment(shift.starting_at);
+        let end = moment(shift.ending_at);
+
+        let diff = moment.duration(end.diff(start)).asHours();
+
+        scheduledHours.push({
+            id: shift.id,
+            scheduled_hours: diff
+        });
+    });
+
+    // Adding all the scheduled hours to form a total
+    let totalScheduledHours = scheduledHours.reduce((accumulator, shift) => {
+        return accumulator + shift.scheduled_hours;
+    }, 0);
+
+    // Formatting the total to 2 decimal places
+    let totalScheduledHoursF = totalScheduledHours.toFixed(2)
+
+    // Worked Hours ----------------------------------------
+
+    // Array to hold worked hours from the single worker
+    let workedHours = [];
+
+    // Calculating the worked hours of each shift and
+    // passing that data as an object to "workedHours"
+    workerClockIns.forEach((shift) => {
+        let start = moment(shift.started_at);
+        let end = moment(shift.ended_at);
+
+        let diff = moment.duration(end.diff(start)).asHours();
+
+        workedHours.push({
+            id: shift.id,
+            worked_hours: diff
+        });
+    });
+
+    // Adding all the worked hours to form a total
+    let totalWorkedHours = workedHours.reduce((accumulator, shift) => {
+        return accumulator + shift.worked_hours;
+    }, 0);
+
+    // Formatting the total to 2 decimal places
+    let totalWorkedHoursF = totalWorkedHours.toFixed(2)
+
+    // Return ------------------------------------------------------------------------------------------------------
+
+    return (
+        <>
+            <Theme.Consumer>
+                {({ bar }) => (
+                    <div className="row d-flex border d-inline-flex justify-content-between py-4 w-100">
+                        {/* Employee Image/Name/Rating Starts */}
+                        <div className="col p-0 d-flex justify-content-center">
+                            <div className="my-auto mr-2">
+                                <Avatar url={worker.user.profile.picture} />
+                            </div>
+                            <div className="ms-2 text-start my-auto d-flex flex-column">
+                                <h5 className="m-0 p-0" align="left">{`${worker.user.first_name} ${worker.user.last_name}`}</h5>
+                                <h5 className="m-0 p-0" align="left">{worker.rating == null ? "No rating available" : worker.rating > 1 ? `Rating: ${worker.rating} stars` : `Rating: ${worker.rating} star`}</h5>
+                            </div>
+                        </div>
+                        {/* Employee Image/Name/Rating Ends */}
+
+                        {/* Scheduled Hours Starts */}
+                        <div className="col p-0 my-auto d-flex justify-content-center">
+                            <h3 className="m-0 p-0">{`Scheduled Hours: ${totalScheduledHoursF}`}</h3>
+                        </div>
+                        {/* Scheduled Hours Ends */}
+
+                        {/* Worked Hours Starts */}
+                        <div className="col p-0 my-auto d-flex justify-content-center">
+
+                            <h3 className="m-0 p-0">{`Worked Hours: ${totalWorkedHoursF}`}</h3>
+                        </div>
+                        {/* Worked Hours Ends */}
+
+                        {/* Invite Button Starts */}
+                        <div className="col p-0 my-auto d-flex justify-content-center">
+                            <Button
+                                className="btn btn-dark bg-dark"
+                                onClick={() =>
+                                    bar.show({
+                                        slug: "invite_talent_to_shift",
+                                        data: worker,
+                                        allowLevels,
+                                    })
+                                }
+                            >
+                                <h5 className="m-0">Invite to Shift</h5>
+                            </Button>
+                        </div>
+                        {/* Invite Button Ends */}
+                    </div>
+                )}
+            </Theme.Consumer>
+        </>
+    );
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_metrics_ratings_Ratings.js.html b/docs/views_metrics_ratings_Ratings.js.html new file mode 100644 index 0000000..1b35271 --- /dev/null +++ b/docs/views_metrics_ratings_Ratings.js.html @@ -0,0 +1,261 @@ + + + + + JSDoc: Source: views/metrics/ratings/Ratings.js + + + + + + + + + + +
+ +

Source: views/metrics/ratings/Ratings.js

+ + + + + + +
+
+
import React, { useEffect, useState } from "react"
+import { PieChart } from "../charts"
+
+/**
+ * @function
+ * @description Creates a pie chart and a table reflecting how many job seekers are in each category of star ratings (1 to 5 stars.)
+ * @since 09.29.22 by Paola Sanchez
+ * @author Paola Sanchez
+ * @requires PieChart
+ * @param {object} props - Contains an array of all shifts, and an array of all workers.
+ */
+export const Ratings = (props) => {
+
+    // Use state to hold list of workers
+    const [workersList, setWorkersList] = useState([])
+
+    // Receiving the props that contain the list of workers
+    const handleProps = async () => {
+
+        // Catching the props when they arrive
+        let workersObj = await props
+
+        // Checking length of list before saving it
+        if (workersObj.workers.length > 0) {
+            // Saving list of workers
+            setWorkersList(workersObj.workers)
+        } else {
+            // Handling error with a message
+            console.log("Waiting for props to arrive")
+        }
+    }
+
+    // Triggering handleProps when props change/arrive
+    useEffect(() => {
+        handleProps()
+    }, [props])
+
+    // Rendering based on length of workersList
+    if (workersList.length > 0) {
+
+        // Preparing the list for the chart data ---------------------------------------
+
+        // Array to hold list of ratings
+        let ratingsList = []
+
+        // Gathering ratings of each worker
+        workersList.forEach((eachWorker) => {
+            ratingsList.push(eachWorker.rating)
+        })
+
+        // Function to make an array of rating quantities
+        const findQuantities = (passedArray) => {
+
+            // Array to hold rating results
+            const results = [];
+
+            // Counting how many times each star rating appears
+            passedArray?.forEach((item) => {
+
+                // Generating indexes
+                const index = results.findIndex((obj) => {
+                    return obj["rating"] === item;
+                });
+
+                // Using the indexes to count rating instances
+                if (index === -1) {
+                    results.push({
+                        rating: item,
+                        qty: 1
+                    });
+
+                } else {
+                    results[index]["qty"]++;
+                }
+            });
+
+            // Returning array
+            return results;
+        };
+
+        // Generating array of rating quantities
+        let ratingsQty = findQuantities(ratingsList);
+
+        // Calculating total of all the quantities
+        let total = ratingsQty.reduce((s, { qty }) => s + qty, 0);
+
+        // Generating and adding percentages as new properties
+        let ratingsPct = ratingsQty.map(({ rating, qty }) => ({
+            rating,
+            qty,
+            pct: ((qty * 100) / total).toFixed(0)
+        }));
+
+        // Organizing objects by numerical order of the "rating" properties
+        let ratingsFinal = ratingsPct.sort((a, b) => (a.rating - b.rating))
+
+        // Moving the first object ("Unavailable Rating") to the last position of the array
+        ratingsFinal.push(ratingsFinal.shift());
+
+        // Generating an object with the totals
+        let totalsObj = { rating: "Total Employees", qty: total, pct: "100" }
+
+        // Adding object with totals to the array
+        ratingsFinal.push(totalsObj)
+
+        // Adding id's to every object in the array
+        ratingsFinal.forEach((item, i) => {
+            item.id = i + 1;
+        });
+
+        // Preparing the chart data ----------------------------------------------------
+
+        // Colors
+        const purple = "#5c00b8";
+        const lightTeal = "#00ebeb";
+        const darkTeal = "#009e9e";
+        const green = "#06ff05";
+        const lightPink = "#eb00eb";
+        const darkPink = "#b200b2";
+
+        // Taking out the "Totals" from the pie chart view
+        let pieData = ratingsFinal.filter((item) => { return item.rating !== "Total Employees" })
+
+        // Preparing data to be passed to the chart component
+        const ratingsData = {
+            labels: pieData.map((data) => { return data.rating === null ? "Unavailable Rating" : ` ${data.rating} Star Employees` }),
+            datasets: [{
+                label: "Employee Ratings",
+                data: pieData.map((data) => data.qty),
+                backgroundColor: [
+                    green, darkTeal, lightPink,
+                    purple, lightTeal, darkPink
+                ],
+            }]
+        }
+
+        // Return ----------------------------------------------------------------------
+
+        return (
+            <div className="row d-flex d-inline-flex justify-content-between w-100">
+                {/* Left Column Starts */}
+                <div className="col">
+                    <div className="row d-flex flex-column justify-content-between">
+                        {/* Ratings Table Starts */}
+                        <div className="col text-center">
+                            <h2 className="mb-4">Employee Ratings Table</h2>
+
+                            <table className="table table-bordered border-dark text-center">
+                                <thead className="thead-dark">
+                                    {/* Table columns */}
+                                    <tr>
+                                        <th scope="col"><h3 className="m-0">Star Rating</h3></th>
+                                        <th scope="col"><h3 className="m-0">Quantity</h3></th>
+                                        <th scope="col"><h3 className="m-0">Percentages</h3></th>
+                                    </tr>
+                                </thead>
+
+                                <tbody>
+                                    {/* Mapping the data to diplay it as table rows */}
+                                    {ratingsFinal.map((item, i) => {
+                                        return item.rating === null ? (
+                                            <tr key={i}>
+                                                <th scope="row"><h3 className="m-0">Unavailable Rating</h3></th>
+                                                <td><h3 className="m-0">{item.qty}</h3></td>
+                                                <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                            </tr>
+                                        ) : item.rating === "Total Employees" ? (
+                                            <tr key={i} style={{ background: "rgba(107, 107, 107, 0.35)" }}>
+                                                <th scope="row"><h3 className="m-0">{item.rating}</h3></th>
+                                                <td><h3 className="m-0">{item.qty}</h3></td>
+                                                <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                            </tr>
+                                        ) : (
+                                            <tr key={i}>
+                                                <th scope="row"><h3 className="m-0">{`${item.rating} Star Employees`}</h3></th>
+                                                <td><h3 className="m-0">{item.qty}</h3></td>
+                                                <td><h3 className="m-0">{`${item.pct}%`}</h3></td>
+                                            </tr>
+                                        )
+                                    })}
+                                </tbody>
+                            </table>
+                        </div>
+                        {/* Ratings Table Ends */}
+                    </div>
+                </div>
+                {/* Left Column Ends */}
+
+                {/* Right Column Starts */}
+                <div className="col">
+                    <div className="row">
+                        {/* Ratings Chart Starts */}
+                        <div className="col text-center">
+                            <h2 className="mb-3">Employee Ratings Chart</h2>
+
+                            <div style={{ height: '26.05rem' }}>
+                                <PieChart pieData={ratingsData} />
+                            </div>
+                        </div>
+                        {/* Ratings Chart Ends */}
+                    </div>
+                </div>
+                {/* Right Column Ends */}
+            </div>
+        )
+    } else {
+        return (
+            <h1>Loading</h1>
+        )
+    }
+}
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_payments.js.html b/docs/views_payments.js.html new file mode 100644 index 0000000..8d994ef --- /dev/null +++ b/docs/views_payments.js.html @@ -0,0 +1,235 @@ + + + + + JSDoc: Source: views/payments.js + + + + + + + + + + +
+ +

Source: views/payments.js

+ + + + + + +
+
+
import React from "react";
+import PropTypes from 'prop-types';
+import { Button } from '../components/index';
+import Flux from "@4geeksacademy/react-flux-dash";
+import { Notify } from 'bc-react-notifier';
+import { makeEmployeePayment } from "../actions";
+import CustomModal from "../components/custom-modal/CustomModal";
+
+export const Payment = (data = {}) => {
+
+    const _defaults = {
+        pay: null,
+        total: null,
+        serialize: function () {
+
+            const newDeduction = {
+            };
+
+            return Object.assign(this, newDeduction);
+        }
+    };
+
+    let _payment = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+            // if (validator.isEmpty(_payment.name)) throw new ValidationError('The deduction name cannot be empty');
+            // if (!_payment.value) throw new ValidationError('Deduction cannot be empty');
+            // if (validator.isEmpty(_payment.description)) throw new ValidationError('The deduction description cannot be empty');
+            // if (!_payment.type) throw new ValidationError('The deduction type cannot be empty');
+            return _payment;
+        },
+        defaults: () => {
+            return _defaults;
+        }
+    };
+};
+
+/**
+ * Make Payment
+ */
+export class MakePayment extends Flux.DashView {
+    
+    render() {
+        const { 
+            onSave, 
+            onCancel, 
+            onChange, 
+            catalog, 
+            formData,
+            error
+         } = this.props;
+         const { pay, paymentInfo, bar } = formData;
+         const employerBankAccounts = paymentInfo && paymentInfo.employer ? paymentInfo.employer.bank_accounts : null;
+         const employeeBankAccounts = pay && pay.employee.bank_accounts && pay.employee.bank_accounts.length > 0;
+        console.log('MakePayment pay: ', pay);
+        console.log('MakePayment error: ', error);
+        console.log('MakePayment paymentInfo: ', paymentInfo);
+
+        console.log("formdata", pay.deduction_list);
+        return (
+        <>
+            {paymentInfo && pay
+        ? <form>
+            <div className="row mt-3">
+                <div className="col-12">
+                    <h4>{`Payment to `}{` ${pay.employee.last_name}, ${pay.employee.first_name}`}</h4>
+                </div>
+            </div>
+            <div className="row">
+                <div className="col-12">
+                    <label>Employee:</label>{` ${pay.employee.last_name}, ${pay.employee.first_name}`}
+                    {/* <p className="m-0 p-0"><span className="badge">{formData.total.status.toLowerCase()}</span></p> */}
+                </div>
+                <div className="col-12">
+                    <label>Regular hours:</label>{` ${Math.round((Number(pay.regular_hours) + Number(pay.over_time)) * 100) / 100 > 40 ? 40 : Math.round((Number(pay.regular_hours) + Number(pay.over_time)) * 100)/100}`}
+                </div>
+                <div className="col-12">
+                    <label>Over time:</label>{` ${Math.round((Number(pay.regular_hours) + Number(pay.over_time)) * 100) / 100 > 40 ? Math.round((Number(pay.regular_hours) + Number(pay.over_time)- 40) * 100 )  / 100  : "-" }`}
+                </div>
+                <div className="col-12">
+                    <label>Earnings:</label>{` ${pay.earnings}`}
+                </div>
+                <div className="col-12">
+                    <label>Taxes:</label>{` ${pay.deductions.toFixed(2)}`}
+                </div>
+                <div className="col-12">
+                    <label>Amount:</label>{` ${pay.amount.toFixed(2)}`}
+                </div>
+            </div>
+            {!pay.paid
+                    ? <div className="row">
+                        <div className="col-12">
+                            <label>Payment methods</label>
+                        </div>
+                        <div className="col-12 payment-cell">
+                            <Button
+                                style={{ width: '200px' }}
+                                color="success"
+                                size="small"
+                                onClick={() => {
+                                    const noti = Notify.add("info", ({ onConfirm }) => <CustomModal onConfirm={onConfirm} title={"Are you sure to pay ?"} />, async (answer) => {
+                                        if(answer){
+                                            try{
+                                                await makeEmployeePayment(
+                                                    pay.id, 
+                                                    "CHECK", 
+                                                    "", 
+                                                    "",
+                                                    pay.deduction_list,
+                                                    pay.deductions
+                                                    );
+                                                noti.remove();
+                                                bar.close();
+                                            }catch(error){
+                                                Notify.error(error.message || error);
+                                            }
+                                        } else{
+                                            noti.remove();
+                                        }
+                                    });
+                                }}>
+                                Check payment
+                            </Button>
+                        </div>
+                        {employeeBankAccounts
+                            ? employerBankAccounts && employerBankAccounts.length > 0
+                                ? employerBankAccounts.map((bankaccount, i) =>
+                                    <div className="col-12 payment-cell" key={i}>
+                                        <Button
+                                            style={{ width: '200px' }}
+                                            color="success"
+                                            size="small"
+                                            onClick={() => {
+                                                const noti = Notify.add("info", ({ onConfirm }) => <CustomModal onConfirm={onConfirm} title={"Are you sure to pay ?"} />, async (answer) => {
+                                                    if(answer){
+                                                        try{
+                                                            await makeEmployeePayment(
+                                                                pay.id, 
+                                                                "ELECTRONIC TRANSFERENCE",
+                                                                bankaccount.id, 
+                                                                pay.employee.bank_accounts[0].id,
+                                                                pay.deduction_list,
+                                                                pay.deductions
+                                                                );
+                                                            noti.remove();
+                                                            bar.close();
+                                                        }catch(error){
+                                                            Notify.error(error.message || error);
+                                                        }
+                                                    } else{
+                                                        noti.remove();
+                                                    }
+                                                });
+                                            }}
+                                            >
+                                            {`${bankaccount.institution_name} ${bankaccount.name}`}
+                                        </Button>
+                                    </div>
+                                )
+                            : <div className="col-12"><label>Employer doesn{`'`}t have any bank accounts</label></div>
+                        : <div className="col-12"><label>Employee doesn{`'`}t have any bank accounts</label></div>}                   
+                    </div>
+        : <div className="row">
+            <div className="col-12">
+                <label>Status:</label>{` Paid`}
+            </div>
+        </div>
+        }
+        </form>
+    : null}
+    </>
+        );
+    }
+}
+
+MakePayment.propTypes = {
+    error: PropTypes.string,
+    action: PropTypes.string,
+    bar: PropTypes.object,
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    formData: PropTypes.object,
+    catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_payroll.js.html b/docs/views_payroll.js.html new file mode 100644 index 0000000..ee123c2 --- /dev/null +++ b/docs/views_payroll.js.html @@ -0,0 +1,4888 @@ + + + + + JSDoc: Source: views/payroll.js + + + + + + + + + + +
+ +

Source: views/payroll.js

+ + + + + + +
+
+
import React, { useState, useEffect, useContext } from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from "prop-types";
+import {
+  store,
+  search,
+  update,
+  fetchSingle,
+  searchMe,
+  processPendingPayrollPeriods,
+  updateProfileMe,
+  updatePayments,
+  createPayment,
+  fetchAllMe,
+  fetchTemporal,
+  remove,
+  create,
+  fetchPeyrollPeriodPayments,
+} from "../actions.js";
+import { GET } from "../utils/api_wrapper";
+import { Session } from "bc-react-session";
+
+import { PDFDocument, rgb } from "pdf-lib";
+import { saveAs } from "file-saver";
+import fw4 from "../../img/fw4.pdf";
+import i9form from "../../img/i92.pdf";
+import loadingURL from "../../img/loading2.gif";
+
+import DateTime from "react-datetime";
+import moment from "moment";
+import {
+  DATETIME_FORMAT,
+  TIME_FORMAT,
+  NOW,
+  TODAY,
+  haversineDistance,
+} from "../components/utils.js";
+import Select from "react-select";
+import { hasTutorial } from "../utils/tutorial";
+
+import { Notify } from "bc-react-notifier";
+
+import { Shift, EditOrAddShift } from "./shifts.js";
+import { Employer } from "./profile.js";
+import { ManageLocations, AddOrEditLocation, Location } from "./locations.js";
+import {
+  EmployeeExtendedCard,
+  ShiftOption,
+  ShiftCard,
+  DeductionExtendedCard,
+  Theme,
+  Button,
+  ShiftOptionSelected,
+  GenericCard,
+  SearchCatalogSelect,
+  Avatar,
+  Toggle,
+  Wizard,
+  StarRating,
+  ListCard,
+} from "../components/index";
+import queryString, { parse } from "query-string";
+
+import TimePicker from "rc-time-picker";
+import "rc-time-picker/assets/index.css";
+
+import Tooltip from "rc-tooltip";
+import "rc-tooltip/assets/bootstrap_white.css";
+
+import GoogleMapReact from "google-map-react";
+
+import { PDFDownloadLink } from "@react-pdf/renderer";
+import { Document, Page } from "react-pdf";
+import TextareaAutosize from "react-textarea-autosize";
+import { PayrollPeriodReport } from "./reports/index.js";
+import { ConsoleView } from "react-device-detect";
+
+const ENTITIY_NAME = "payroll";
+
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getPayrollInitialFilters = (catalog) => {
+  let query = queryString.parse(window.location.search);
+  if (typeof query == "undefined")
+    return {
+      starting_at: TODAY(),
+      ending_at: new Date().setDate(TODAY().getDate() - 7),
+    };
+  return {
+    starting_at: query.starting_at,
+    ending_at: query.ending_at,
+  };
+};
+
+export const Clockin = (data) => {
+  const _defaults = {
+    author: null,
+    employee: null,
+    shift: null,
+    created_at: null,
+    updated_at: null,
+    started_at: TODAY(),
+    ended_at: TODAY(),
+    distance_in_miles: 0,
+    distance_out_miles: 0,
+    latitude: [],
+    longitude: [],
+    status: "PENDING",
+    serialize: function () {
+      const newObj = {
+        shift:
+          !this.shift || typeof this.shift.id === "undefined"
+            ? this.shift
+            : this.shift.id,
+        employee:
+          !this.employee || typeof this.employee.id === "undefined"
+            ? this.employee
+            : this.employee.id,
+      };
+
+      return Object.assign(this, newObj);
+    },
+    unserialize: function () {
+      const dataType = typeof this.started_at;
+      //if its already serialized
+      if (
+        typeof this.shift == "object" &&
+        ["number", "string"].indexOf(dataType) == -1
+      )
+        return this;
+
+      const newObject = {
+        shift:
+          typeof this.shift != "object"
+            ? store.get("shift", this.shift)
+            : Shift(this.shift).defaults().unserialize(),
+        employee:
+          typeof this.employee != "object"
+            ? store.get("employees", this.employee)
+            : this.employee,
+        started_at:
+          this.started_at && !moment.isMoment(this.started_at)
+            ? moment(this.started_at)
+            : this.started_at,
+        ended_at:
+          this.ended_at && !moment.isMoment(this.ended_at)
+            ? moment(this.ended_at)
+            : this.ended_at,
+        latitude_in: parseFloat(this.latitude_in),
+        longitude_in: parseFloat(this.longitude_in),
+        latitude_out: parseFloat(this.latitude_out),
+        longitude_out: parseFloat(this.longitude_out),
+        distance_in_miles: parseFloat(this.distance_in_miles),
+        distance_out_miles: parseFloat(this.distance_out_miles),
+      };
+
+      return Object.assign(this, newObject);
+    },
+  };
+
+  let _checkin = Object.assign(_defaults, data);
+  return {
+    get: () => {
+      return _checkin;
+    },
+    validate: () => {
+      const start = _checkin.stared_at;
+      const finish = _checkin.ended_at;
+
+      //if(SHIFT_POSSIBLE_STATUS.indexOf(_shift.status) == -1) throw new Error('Invalid status "'+_shift.status+'" for shift');
+
+      return _checkin;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _formCheckin = {
+        id: _checkin.id.toString(),
+      };
+      return _formCheckin;
+    },
+  };
+};
+
+export const PayrollPeriod = (data) => {
+  const _defaults = {
+    employer: null,
+    id: null,
+    length: 0,
+    length_type: "DAYS",
+    payments: [],
+    starting_at: null,
+    status: null,
+    serialize: function () {
+      const newObj = {
+        employer:
+          !this.employer || typeof this.employer.id === "undefined"
+            ? this.employer
+            : this.employer.id,
+      };
+
+      return Object.assign(this, newObj);
+    },
+    unserialize: function () {
+      const newObject = {
+        //shift: (typeof this.shift != 'object') ? store.get('shift', this.shift) : Shift(this.shift).defaults().unserialize(),
+      };
+
+      return Object.assign(this, newObject);
+    },
+  };
+
+  let _payment = Object.assign(_defaults, data);
+  return {
+    get: () => {
+      return _payment;
+    },
+    validate: () => {
+      const start = _payment.starting_at;
+      const finish = _payment.ending_at;
+
+      //if(SHIFT_POSSIBLE_STATUS.indexOf(_shift.status) == -1) throw new Error('Invalid status "'+_shift.status+'" for shift');
+
+      return _payment;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _formCheckin = {
+        id: _payment.id.toString(),
+      };
+      return _formCheckin;
+    },
+  };
+};
+
+export const Payment = (data) => {
+  const _defaults = {
+    //employer: null,
+    //id: null,
+    serialize: function () {
+      const newObj = {
+        id: this.id,
+        regular_hours: this.regular_hours,
+        over_time: this.over_time,
+        hourly_rate: this.hourly_rate,
+        total_amount: this.total_amount,
+        breaktime_minutes: 0,
+        status: this.status,
+        splited_payment: this.splited_payment,
+        payroll_period:
+          !this.employer || typeof this.employer.id === "undefined"
+            ? this.employer
+            : this.employer.id,
+        employer:
+          !this.employer || typeof this.employer.id === "undefined"
+            ? this.employer
+            : this.employer.id,
+        employee:
+          !this.employee || typeof this.employee.id === "undefined"
+            ? this.employee
+            : this.employee.id,
+        shift:
+          !this.shift || typeof this.shift.id === "undefined"
+            ? this.shift
+            : this.shift.id,
+        clockin:
+          !this.clockin || typeof this.clockin.id === "undefined"
+            ? this.clockin
+            : this.clockin.id,
+      };
+
+      return Object.assign(this, newObj);
+    },
+    unserialize: function () {
+      const newObject = {
+        //shift: (typeof this.shift != 'object') ? store.get('shift', this.shift) : Shift(this.shift).defaults().unserialize(),
+        created_at:
+          this.created_at && !moment.isMoment(this.created_at)
+            ? moment(this.created_at)
+            : this.created_at,
+        updated_at:
+          this.updated_at && !moment.isMoment(this.updated_at)
+            ? moment(this.updated_at)
+            : this.updated_at,
+      };
+
+      return Object.assign(this, newObject);
+    },
+  };
+
+  let _payment = Object.assign(_defaults, data);
+  return {
+    get: () => {
+      return _payment;
+    },
+    validate: () => {
+      //if(SHIFT_POSSIBLE_STATUS.indexOf(_shift.status) == -1) throw new Error('Invalid status "'+_shift.status+'" for shift');
+      return _payment;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _form = {
+        id: _payment.id.toString(),
+      };
+      return _form;
+    },
+  };
+};
+
+export class PayrollSettings extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      employer: Employer().defaults(),
+      deductions: [],
+      runTutorial: hasTutorial(),
+      steps: [
+        {
+          content: (
+            <div>
+              <h2>This is your payroll setting page</h2>
+              <p>Lets start by updating your preferences </p>
+            </div>
+          ),
+          placement: "center",
+
+          styles: {
+            options: {
+              zIndex: 10000,
+            },
+            buttonClose: {
+              display: "none",
+            },
+          },
+          locale: { skip: "Skip tutorial" },
+          target: "body",
+        },
+        {
+          target: "#payroll_run",
+          content:
+            "Edit the company payroll period. When it will begin and when it ends",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#payroll_clockin",
+          content:
+            "Edit the amount of time you like to give your employees before/after to clokin. This option will help to prevent early clock ins.",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#payroll_automatic",
+          content: "You can choose to enable automatic checkout",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+
+        {
+          target: "#button_save",
+          content: "Save your progress",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: false,
+          disableCloseOnEsc: false,
+          disableOverlayClose: false,
+          disableScrollParentFix: false,
+        },
+      ],
+    };
+  }
+
+  setEmployer(newEmployer) {
+    const employer = Object.assign(this.state.employer, newEmployer);
+    this.setState({ employer });
+  }
+
+  componentDidMount() {
+    const deductions = store.getState("deduction");
+    if (!deductions) {
+      searchMe("deduction");
+    } else {
+      this.setState({ deductions });
+    }
+    fetchTemporal("employers/me", "current_employer");
+    this.subscribe(store, "current_employer", (employer) => {
+      this.setState({ employer });
+    });
+    this.subscribe(store, "deduction", (deductions) => {
+      this.setState({ deductions });
+    });
+  }
+  callback = (data) => {
+    // if(data.action == 'next' && data.index == 0){
+    //     this.props.history.push("/payroll");
+
+    // }
+    if (data.status == "skipped") {
+      const session = Session.get();
+      updateProfileMe({ show_tutorial: false });
+
+      const profile = Object.assign(session.payload.user.profile, {
+        show_tutorial: false,
+      });
+      const user = Object.assign(session.payload.user, { profile });
+      Session.setPayload({ user });
+    }
+    if (data.type == "tour:end") {
+      const session = Session.get();
+      updateProfileMe({ show_tutorial: false });
+
+      const profile = Object.assign(session.payload.user.profile, {
+        show_tutorial: false,
+      });
+      const user = Object.assign(session.payload.user, { profile });
+      Session.setPayload({ user });
+      this.props.history.push("/");
+    }
+  };
+  render() {
+    const autoClockout =
+      this.state.employer.maximum_clockout_delay_minutes == null ? false : true;
+    const weekday =
+      this.state.employer.payroll_period_starting_time.isoWeekday();
+    let nextDate = this.state.employer.payroll_period_starting_time.clone();
+    while (nextDate.isBefore(NOW())) nextDate = nextDate.add(7, "days");
+
+    return (
+      <Theme.Consumer>
+        {({ bar }) => (
+          <div className="p-1 listcontents company-payroll-settings">
+            <Wizard
+              continuous
+              steps={this.state.steps}
+              run={this.state.runTutorial}
+              callback={(data) => this.callback(data)}
+              disableCloseOnEsc={true}
+              disableOverlayClose={true}
+              disableScrollParentFix={true}
+              styles={{
+                options: {
+                  primaryColor: "#000",
+                },
+              }}
+            />
+            <h1>
+              <span id="company_details">Your Payroll Settings</span>
+            </h1>
+            <div className="row mt-2">
+              <div className="col-12">
+                <h4>
+                  Next payroll will run on{" "}
+                  {nextDate.format("dddd, MMMM Do YYYY, h:mm a")}
+                </h4>
+              </div>
+            </div>
+            <form>
+              <div className="row mt-2">
+                <div className="col-12" id="payroll_run">
+                  <label className="d-block">
+                    When do you want your payroll to run?
+                  </label>
+                  <span>Every </span>
+                  <select
+                    className="form-control"
+                    style={{ width: "100px", display: "inline-block" }}
+                  >
+                    <option>Week</option>
+                  </select>
+                  <span> starting </span>
+                  <select
+                    value={weekday || 1}
+                    className="form-control"
+                    style={{ width: "120px", display: "inline-block" }}
+                    onChange={(e) => {
+                      const diff = e.target.value - weekday;
+                      let newDate =
+                        this.state.employer.payroll_period_starting_time
+                          .clone()
+                          .add(diff, "days");
+                      this.setEmployer({
+                        payroll_period_starting_time: newDate,
+                      });
+                    }}
+                  >
+                    <option value={1}>Mondays</option>
+                    <option value={2}>Tuesdays</option>
+                    <option value={3}>Wednesdays</option>
+                    <option value={4}>Thursdays</option>
+                    <option value={5}>Fridays</option>
+                    <option value={6}>Saturdays</option>
+                    <option value={7}>Sundays</option>
+                  </select>
+                  <span> at </span>
+                  <DateTime
+                    dateFormat={false}
+                    styles={{ width: "100px", display: "inline-block" }}
+                    timeFormat={DATETIME_FORMAT}
+                    timeConstraints={{ minutes: { step: 15 } }}
+                    value={this.state.employer.payroll_period_starting_time}
+                    renderInput={(properties) => {
+                      const { value, ...rest } = properties;
+                      return (
+                        <input
+                          value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                          {...rest}
+                        />
+                      );
+                    }}
+                    onChange={(value) => {
+                      const starting = moment(
+                        this.state.employer.payroll_period_starting_time.format(
+                          "MM-DD-YYYY"
+                        ) +
+                          " " +
+                          value.format("hh:mm a"),
+                        "MM-DD-YYYY hh:mm a"
+                      );
+                      this.setEmployer({
+                        payroll_period_starting_time: starting,
+                      });
+                    }}
+                  />
+                </div>
+              </div>
+              <div className="row">
+                <div className="col-12" id="payroll_clockin">
+                  <label className="d-block">
+                    When can talents start clocking in?
+                  </label>
+                  <select
+                    value={this.state.employer.maximum_clockin_delta_minutes}
+                    className="form-control"
+                    style={{ width: "100px", display: "inline-block" }}
+                    onChange={(e) =>
+                      this.setEmployer({
+                        maximum_clockin_delta_minutes: isNaN(e.target.value)
+                          ? null
+                          : e.target.value,
+                        timeclock_warning: true,
+                      })
+                    }
+                  >
+                    <option value={0}>Select</option>
+                    <option value={5}>5 min</option>
+                    <option value={10}>10 min</option>
+                    <option value={15}>15 min</option>
+                    <option value={30}>30 min</option>
+                    <option value={45}>45 min</option>
+                    <option value={60}>1 hour</option>
+                  </select>
+                  <span> before or after the starting time of the shift</span>
+                </div>
+              </div>
+              <div id="payroll_automatic">
+                <div className="row mt-2">
+                  <div className="col-12">
+                    <label className="d-block">
+                      Do you want automatic checkout?
+                    </label>
+                    <select
+                      value={autoClockout}
+                      className="form-control"
+                      style={{ width: "450px", display: "inline-block" }}
+                      onChange={(e) => {
+                        this.setEmployer({
+                          maximum_clockout_delay_minutes:
+                            e.target.value == "true" ? 10 : null,
+                          timeclock_warning: true,
+                        });
+                      }}
+                    >
+                      <option value={true}>
+                        Only if the talent forgets to checkout
+                      </option>
+                      <option value={false}>
+                        No, leave the shift active until the talent checkouts
+                      </option>
+                    </select>
+                    {!autoClockout ? (
+                      ""
+                    ) : (
+                      <span>
+                        , wait
+                        <input
+                          type="number"
+                          style={{ width: "60px" }}
+                          className="form-control d-inline-block ml-2 mr-2"
+                          value={
+                            this.state.employer.maximum_clockout_delay_minutes
+                          }
+                          onChange={(e) =>
+                            this.setEmployer({
+                              maximum_clockout_delay_minutes: e.target.value,
+                              timeclock_warning: true,
+                            })
+                          }
+                        />
+                        min to auto checkout
+                      </span>
+                    )}
+                  </div>
+                </div>
+                {this.state.employer.timeclock_warning && (
+                  <div className="alert alert-warning p-2 mt-3">
+                    Apply time clock settings to:
+                    <select
+                      value={this.state.employer.retroactive}
+                      className="form-control w-100"
+                      style={{ width: "100px", display: "inline-block" }}
+                      onChange={(e) =>
+                        this.setEmployer({
+                          retroactive: e.target.value === "true" ? true : false,
+                        })
+                      }
+                    >
+                      <option value={false}>
+                        Only new shifts (from now on)
+                      </option>
+                      <option value={true}>
+                        All shifts (including previously created)
+                      </option>
+                    </select>
+                  </div>
+                )}
+              </div>
+              {/* <div className="row mt-2">
+                            <div className="col-12" id="payroll_deduction">
+                                <label>Deductions</label>
+                                <div className="p-1 listcontents">
+                                    {this.state.deductions.length > 0
+                                        ? <table className="table table-striped payroll-summary">
+                                            <thead>
+                                                <tr>
+                                                    <th>Name</th>
+                                                    <th>Deduction</th>
+                                                    <th>Status</th>
+                                                    <th>Description</th>
+                                                    <th></th>
+                                                </tr>
+                                            </thead>
+                                            <tbody>
+                                                {this.state.deductions.map((deduction, i) => (
+                                                    <DeductionExtendedCard
+                                                        key={i}
+                                                        deduction={deduction}
+                                                        onEditClick={() => bar.show({
+                                                            slug: "update_deduction",
+                                                            data: deduction
+                                                        })}
+                                                        onDelete={() => {
+                                                            const noti = Notify.info("Are you sure you want to delete this deduction?", (answer) => {
+                                                                if (answer) remove('deduction', deduction);
+                                                                noti.remove();
+                                                            });
+                                                        }}
+                                                    >
+                                                    </DeductionExtendedCard>
+                                                ))}
+                                            </tbody>
+                                        </table>
+                                        : <p>No deductions yet</p>
+                                    }
+                                </div>
+                                <Button
+                                    size="small"
+                                    onClick={() => bar.show({
+                                        slug: "create_deduction",
+                                        data: {
+                                            name: "",
+                                            active: false,
+                                            value: null,
+                                            description: "",
+                                            type: "PERCENTAGE"
+                                        }
+                                    })}
+                                >
+                                    Add Deduction
+                                </Button>
+                            </div>
+                        </div> */}
+              <div className="mt-4 text-right">
+                <button
+                  id="button_save"
+                  type="button"
+                  className="btn btn-primary"
+                  onClick={() =>
+                    update(
+                      { path: "employers/me", event_name: "current_employer" },
+                      Employer(this.state.employer).validate().serialize()
+                    ).catch((e) => Notify.error(e.message || e))
+                  }
+                >
+                  Save Payroll Settings
+                </button>
+              </div>
+            </form>
+          </div>
+        )}
+      </Theme.Consumer>
+    );
+  }
+}
+
+/**
+ * EditOrAddExpiredShift
+ */
+export const EditOrAddExpiredShift = ({
+  onSave,
+  onCancel,
+  onChange,
+  catalog,
+  formData,
+  error,
+  oldShift,
+}) => {
+  const { bar } = useContext(Theme.Context);
+
+  useEffect(() => {
+    const venues = store.getState("venues");
+    const favlists = store.getState("favlists");
+    if (!venues || !favlists) fetchAllMe(["venues", "favlists"]);
+  }, []);
+  const expired =
+    moment(formData.starting_at).isBefore(NOW()) ||
+    moment(formData.ending_at).isBefore(NOW());
+
+  const validating_minimum = moment(formData.starting_at).isBefore(
+    formData.period_starting
+  );
+  const validating_maximum = moment(formData.ending_at).isAfter(
+    formData.period_ending
+  );
+
+  return (
+    <form>
+      <div className="row">
+        <div className="col-12">
+          {formData.hide_warnings === true ? null : formData.status ==
+              "DRAFT" && !error ? (
+            <div className="alert alert-warning d-inline">
+              <i className="fas fa-exclamation-triangle"></i> This shift is a
+              draft
+            </div>
+          ) : formData.status != "UNDEFINED" && !error ? (
+            <div className="alert alert-success">
+              This shift is published, therefore{" "}
+              <strong>it needs to be unpublished</strong> before it can be
+              updated
+            </div>
+          ) : (
+            ""
+          )}
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12">
+          <label>Looking for</label>
+          <Select
+            value={catalog.positions.find(
+              (pos) => pos.value == formData.position
+            )}
+            onChange={(selection) =>
+              onChange({
+                position: selection.value.toString(),
+                has_sensitive_updates: true,
+              })
+            }
+            options={catalog.positions}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-6">
+          <label>How many?</label>
+          <input
+            type="number"
+            className="form-control"
+            value={formData.maximum_allowed_employees}
+            onChange={(e) => {
+              if (parseInt(e.target.value, 10) > 0) {
+                if (
+                  oldShift &&
+                  oldShift.employees.length > parseInt(e.target.value, 10)
+                )
+                  Notify.error(
+                    `${oldShift.employees.length} talents are scheduled to work on this shift already, delete scheduled employees first.`
+                  );
+                else onChange({ maximum_allowed_employees: e.target.value });
+              }
+            }}
+          />
+        </div>
+        <div className="col-6">
+          <label>Price / hour</label>
+          <input
+            type="number"
+            className="form-control"
+            value={formData.minimum_hourly_rate}
+            onChange={(e) =>
+              onChange({
+                minimum_hourly_rate: e.target.value,
+                has_sensitive_updates: true,
+              })
+            }
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12">
+          <label className="mb-1">Dates</label>
+          <div className="input-group">
+            <DateTime
+              timeFormat={false}
+              className="shiftdate-picker"
+              closeOnSelect={true}
+              viewDate={formData.starting_at}
+              value={formData.starting_at}
+              isValidDate={(current) => {
+                return (
+                  current.isSameOrAfter(
+                    moment(formData.period_starting).startOf("day")
+                  ) &&
+                  current.isSameOrBefore(
+                    moment(formData.period_ending).startOf("day")
+                  )
+                );
+              }}
+              renderInput={(properties) => {
+                const { value, ...rest } = properties;
+                return (
+                  <input
+                    value={value.match(/\d{2}\/\d{2}\/\d{4}/gm)}
+                    {...rest}
+                  />
+                );
+              }}
+              onChange={(value) => {
+                const getRealDate = (start, end) => {
+                  if (typeof start == "string") value = moment(start);
+
+                  const starting = moment(
+                    start.format("MM-DD-YYYY") + " " + start.format("hh:mm a"),
+                    "MM-DD-YYYY hh:mm a"
+                  );
+                  var ending = moment(
+                    start.format("MM-DD-YYYY") + " " + end.format("hh:mm a"),
+                    "MM-DD-YYYY hh:mm a"
+                  );
+
+                  if (typeof starting !== "undefined" && starting.isValid()) {
+                    if (ending.isBefore(starting)) {
+                      ending = ending.add(1, "days");
+                    }
+
+                    return { starting_at: starting, ending_at: ending };
+                  }
+                  return null;
+                };
+
+                const mainDate = getRealDate(value, formData.ending_at);
+                const multipleDates = !Array.isArray(formData.multiple_dates)
+                  ? []
+                  : formData.multiple_dates.map((d) =>
+                      getRealDate(d.starting_at, d.ending_at)
+                    );
+                onChange({
+                  ...mainDate,
+                  multiple_dates: multipleDates,
+                  has_sensitive_updates: true,
+                });
+              }}
+            />
+          </div>
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-6">
+          <label>From</label>
+          <DateTime
+            dateFormat={false}
+            timeFormat={DATETIME_FORMAT}
+            closeOnTab={true}
+            timeConstraints={{ minutes: { step: 15 } }}
+            value={formData.starting_at}
+            renderInput={(properties) => {
+              const { value, ...rest } = properties;
+              return (
+                <input
+                  value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                  {...rest}
+                />
+              );
+            }}
+            onChange={(value) => {
+              if (typeof value == "string") value = moment(value);
+
+              const getRealDate = (start, end) => {
+                const starting = moment(
+                  start.format("MM-DD-YYYY") + " " + value.format("hh:mm a"),
+                  "MM-DD-YYYY hh:mm a"
+                );
+                var ending = moment(end);
+                if (typeof starting !== "undefined" && starting.isValid()) {
+                  if (ending.isBefore(starting)) {
+                    ending = ending.add(1, "days");
+                  }
+
+                  return { starting_at: starting, ending_at: ending };
+                }
+                return null;
+              };
+
+              const mainDate = getRealDate(
+                formData.starting_at,
+                formData.ending_at
+              );
+              const multipleDates = !Array.isArray(formData.multiple_dates)
+                ? []
+                : formData.multiple_dates.map((d) =>
+                    getRealDate(d.starting_at, d.ending_at)
+                  );
+              onChange({
+                ...mainDate,
+                multiple_dates: multipleDates,
+                has_sensitive_updates: true,
+              });
+            }}
+          />
+        </div>
+        <div className="col-6">
+          <label>
+            To{" "}
+            {formData.ending_at.isBefore(formData.starting_at) && "(next day)"}
+          </label>
+          <DateTime
+            className="picker-left"
+            dateFormat={false}
+            timeFormat={DATETIME_FORMAT}
+            timeConstraints={{ minutes: { step: 15 } }}
+            value={formData.ending_at}
+            renderInput={(properties) => {
+              const { value, ...rest } = properties;
+              return (
+                <input
+                  value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                  {...rest}
+                />
+              );
+            }}
+            onChange={(value) => {
+              if (typeof value == "string") value = moment(value);
+
+              const getRealDate = (start, end) => {
+                const starting = start;
+                var ending = moment(
+                  start.format("MM-DD-YYYY") + " " + value.format("hh:mm a"),
+                  "MM-DD-YYYY hh:mm a"
+                );
+
+                if (typeof starting !== "undefined" && starting.isValid()) {
+                  if (ending.isBefore(starting)) {
+                    ending = ending.add(1, "days");
+                  }
+
+                  return { starting_at: starting, ending_at: ending };
+                }
+                return null;
+              };
+
+              const mainDate = getRealDate(
+                formData.starting_at,
+                formData.ending_at
+              );
+              const multipleDates = !Array.isArray(formData.multiple_dates)
+                ? []
+                : formData.multiple_dates.map((d) =>
+                    getRealDate(d.starting_at, d.ending_at)
+                  );
+              onChange({
+                ...mainDate,
+                multiple_dates: multipleDates,
+                has_sensitive_updates: true,
+              });
+            }}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12">
+          <label>Location</label>
+          <Select
+            value={catalog.venues.find((ven) => ven.value == formData.venue)}
+            options={[
+              {
+                label: "Add a location",
+                value: "new_venue",
+                component: AddOrEditLocation,
+              },
+            ].concat(catalog.venues)}
+            onChange={(selection) => {
+              if (selection.value == "new_venue")
+                bar.show({ slug: "create_location", allowLevels: true });
+              else
+                onChange({
+                  venue: selection.value.toString(),
+                  has_sensitive_updates: true,
+                });
+            }}
+          />
+        </div>
+      </div>
+      <div className="row mt-3">
+        <div className="col-12">
+          <h4>Who was supposed to work on this shift?</h4>
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12">
+          {/* <label>Search people in JobCore:</label> */}
+          <SearchCatalogSelect
+            isMulti={true}
+            value={formData.employeesToAdd}
+            onChange={(selections) => {
+              onChange({ employeesToAdd: selections });
+            }}
+            searchFunction={(search) =>
+              new Promise((resolve, reject) =>
+                GET("catalog/employees?full_name=" + search)
+                  .then((talents) =>
+                    resolve(
+                      [
+                        {
+                          label: `${
+                            talents.length == 0 ? "No one found: " : ""
+                          }`,
+                        },
+                      ].concat(talents)
+                    )
+                  )
+                  .catch((error) => reject(error))
+              )
+            }
+          />
+        </div>
+      </div>
+
+      <div className="btn-bar">
+        <button
+          type="button"
+          className="btn btn-success"
+          onChange={(value) => {
+            const getRealDate = (start, end) => {
+              if (typeof start == "string") value = moment(start);
+
+              const starting = moment(
+                start.format("MM-DD-YYYY") + " " + start.format("hh:mm a"),
+                "MM-DD-YYYY hh:mm a"
+              );
+              var ending = moment(
+                start.format("MM-DD-YYYY") + " " + end.format("hh:mm a"),
+                "MM-DD-YYYY hh:mm a"
+              );
+
+              if (typeof starting !== "undefined" && starting.isValid()) {
+                if (ending.isBefore(starting)) {
+                  ending = ending.add(1, "days");
+                }
+
+                return { starting_at: starting, ending_at: ending };
+              }
+              return null;
+            };
+            const mainDate = getRealDate(value, formData.ending_at);
+            onChange({ ...mainDate, has_sensitive_updates: true });
+          }}
+          onClick={() =>
+            validating_maximum || validating_minimum
+              ? Notify.error("Cannot create shift before payroll time or after")
+              : onSave({
+                  executed_action: "create_expired_shift",
+                  status: "OPEN",
+                })
+          }
+        >
+          Save and publish
+        </button>
+      </div>
+    </form>
+  );
+};
+EditOrAddExpiredShift.propTypes = {
+  error: PropTypes.string,
+  oldShift: PropTypes.object,
+  bar: PropTypes.object,
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+EditOrAddExpiredShift.defaultProps = {
+  oldShift: null,
+};
+export const ManagePayroll = () => {
+  const { bar } = useContext(Theme.Context);
+
+  return <div className="p-1 listcontents">Pick a period</div>;
+};
+
+export const PayrollPeriodDetails = ({ match, history }) => {
+  const [employer, setEmployer] = useState(store.getState("current_employer"));
+  const [period, setPeriod] = useState(null);
+  const [form, setForm] = useState("");
+  const [payments, setPayments] = useState([]);
+  const [formLoading, setFormLoading] = useState(false);
+  const [open, setOpen] = useState(true);
+
+  const { bar } = useContext(Theme.Context);
+  useEffect(() => {
+    const employerSub = store.subscribe("current_employer", (employer) =>
+      setEmployer(employer)
+    );
+    if (match.params.period_id !== undefined)
+      fetchSingle("payroll-periods", match.params.period_id).then((_period) => {
+        setPeriod(_period);
+        setPayments(_period.payments);
+      });
+
+    const removeHistoryListener = history.listen((data) => {
+      const period = /\/payroll\/period\/(\d+)/gm;
+      const periodMatches = period.exec(data.pathname);
+      if (periodMatches)
+        fetchSingle("payroll-periods", periodMatches[1]).then((_period) => {
+          setPeriod(_period);
+          setPayments(_period.payments);
+        });
+    });
+
+    return () => {
+      employerSub.unsubscribe();
+      removeHistoryListener();
+    };
+  }, []);
+
+  if (!employer || !period) return "Loading...";
+  if (
+    !employer.payroll_configured ||
+    !moment.isMoment(employer.payroll_period_starting_time)
+  ) {
+    return (
+      <div className="p-1 listcontents text-center">
+        <h3>Please setup your payroll settings first.</h3>
+        <Button
+          color="success"
+          onClick={() => history.push("/payroll/settings")}
+        >
+          Setup Payroll Settings
+        </Button>
+      </div>
+    );
+  }
+
+  let groupedPayments = {};
+  for (let i = 0; i < payments.length; i++) {
+    const pay = payments[i];
+    if (typeof groupedPayments[pay.employee.id] === "undefined") {
+      groupedPayments[pay.employee.id] = {
+        employee: pay.employee,
+        payments: [],
+      };
+    }
+    groupedPayments[pay.employee.id].payments.push(pay);
+  }
+  groupedPayments = Object.keys(groupedPayments).map(
+    (id) => groupedPayments[id]
+  );
+
+  function parseToTime(num) {
+    var decimalTimeString = num;
+    var decimalTime = parseFloat(decimalTimeString);
+    decimalTime = decimalTime * 60 * 60;
+    var hours = Math.floor(decimalTime / (60 * 60));
+    decimalTime = decimalTime - hours * 60 * 60;
+    var minutes = Math.floor(decimalTime / 60);
+
+    if (hours < 10) {
+      hours = "0" + hours;
+    }
+    if (minutes < 10) {
+      minutes = "0" + minutes;
+    }
+    return "" + hours + ":" + minutes;
+  }
+
+  async function getEmployeeDocumet(emp, type) {
+    setFormLoading(true);
+    setForm(null);
+    const id = emp.employee.id;
+
+    const w4form = await GET("employers/me/" + "w4-form" + "/" + id);
+    const i9form = await GET("employers/me/" + "i9-form" + "/" + id);
+    const employeeDocument = await GET(
+      "employers/me/" + "employee-documents" + "/" + id
+    );
+
+    const data = {
+      w4form: w4form[0],
+      i9form: i9form[0],
+      employeeDocument: employeeDocument[0] || "",
+      employeeDocument2: employeeDocument[1] || "",
+    };
+
+    if (type === "w4") fillForm(data);
+    else if (type === "i9") fillFormI9(data);
+  }
+
+  async function fillForm(data) {
+    if (data) {
+      const signature = data.w4form.employee_signature;
+      const png = `data:image/png;base64,${signature}`;
+      const formUrl =
+        "https://api.vercel.com/now/files/20f93230bb41a5571f15a12ca0db1d5b20dd9ce28ca9867d20ca45f6651cca0f/fw4.pdf";
+
+      const formPdfBytes = await fetch(formUrl).then((res) =>
+        res.arrayBuffer()
+      );
+      const pngUrl = png;
+
+      var pngImageBytes;
+      var pdfDoc = await PDFDocument.load(formPdfBytes);
+      var pngImage;
+      if (signature) {
+        pngImageBytes = await fetch(pngUrl).then((res) => res.arrayBuffer());
+
+        pngImage = await pdfDoc.embedPng(pngImageBytes);
+      }
+      const pages = pdfDoc.getPages();
+      const firstPage = pages[0];
+
+      const { width, height } = firstPage.getSize();
+
+      const form = pdfDoc.getForm();
+
+      var pngDims;
+      if (pngImage) pngDims = pngImage.scale(0.18);
+
+      const nameField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_01[0]"
+      );
+      const lastNameField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_02[0]"
+      );
+      const socialSecurityField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_05[0]"
+      );
+      const addressField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_03[0]"
+      );
+      const cityField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_04[0]"
+      );
+      const fillingFieldSingle = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[0]"
+      );
+      const fillingFieldMarried = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[1]"
+      );
+      const fillingFieldHead = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[2]"
+      );
+      const multipleJobsField = form.getCheckBox(
+        "topmostSubform[0].Page1[0].Step2c[0].c1_2[0]"
+      );
+      const step3aField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step3_ReadOrder[0].f1_06[0]"
+      );
+      const step3bField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step3_ReadOrder[0].f1_07[0]"
+      );
+      const step3cField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_08[0]"
+      );
+      const step4aField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_09[0]"
+      );
+      const step4bField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_10[0]"
+      );
+      const step4cField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_11[0]"
+      );
+      const employerField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_13[0]"
+      );
+      const employmentDateField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_14[0]"
+      );
+      const einField = form.getTextField("topmostSubform[0].Page1[0].f1_15[0]");
+
+      nameField.setText(data.i9form.first_name);
+      lastNameField.setText(data.i9form.last_name);
+      socialSecurityField.setText(data.i9form.social_security);
+      addressField.setText(data.i9form.address);
+      cityField.setText(data.i9form.city);
+
+      if (data.w4form.filing_status == "SINGLE") fillingFieldSingle.check();
+      else if (data.w4form.filing_status == "MARRIED")
+        fillingFieldMarried.check();
+      else if (data.w4form.filing_status == "HEAD") fillingFieldHead.check();
+
+      if (data.w4form.step2c) multipleJobsField.check();
+
+      step3aField.setText(data.w4form.dependant3b);
+      step3bField.setText(data.w4form.dependant3c);
+
+      if (data.w4form.dependant3b && data.w4form.dependant3c) {
+        var totalDependant =
+          +data.w4form.dependant3b + +data.w4form.dependant3c;
+        step3cField.setText(totalDependant.toString());
+      }
+
+      step4aField.setText(data.w4form.step4a);
+      step4bField.setText(data.w4form.step4b);
+      step4cField.setText(data.w4form.step4c);
+      employerField.setText(
+        "JobCore Inc, 270 Catalonia AveCoral Gables, FL 33134"
+      );
+      employmentDateField.setText(
+        moment(data.w4form.updated_at).format("MM/DD/YYYY")
+      );
+      einField.setText("83-1919066");
+
+      firstPage.drawText(moment(data.w4form.created_at).format("MM/DD/YYYY"), {
+        x: 470,
+        y: firstPage.getHeight() / 5.5,
+        size: 14,
+        color: rgb(0, 0, 0),
+      });
+
+      if (pngImage) {
+        firstPage.drawImage(pngImage, {
+          x: firstPage.getWidth() / 7 - pngDims.width / 2 + 75,
+          y: firstPage.getHeight() / 4.25 - pngDims.height,
+          width: pngDims.width,
+          height: pngDims.height,
+        });
+      }
+
+      const pdfBytes = await pdfDoc.save();
+      var blob = new Blob([pdfBytes], { type: "application/pdf" });
+
+      const fileURL = URL.createObjectURL(blob);
+      setForm(fileURL);
+      setFormLoading(false);
+
+      //   window.open(blob);
+      //   saveAs(blob, `${data.i9form.first_name + "_" + data.i9form.last_name+"_W4"}.pdf`);
+    }
+  }
+  async function fillFormI9(data) {
+    if (data) {
+      const signature = data.i9form.employee_signature;
+      const png = `data:image/png;base64,${signature}`;
+      const formUrl =
+        "https://api.vercel.com/now/files/5032a373d2e112174680444f0aac149210e4ac4c3c3b55144913e319cfa72bd4/i92.pdf";
+      const formPdfBytes = await fetch(formUrl).then((res) =>
+        res.arrayBuffer()
+      );
+
+      const pngUrl = png;
+      var pngImageBytes;
+      var pngImage;
+      const pdfDoc = await PDFDocument.load(formPdfBytes);
+      if (signature) {
+        pngImageBytes = await fetch(pngUrl).then((res) => res.arrayBuffer());
+        pngImage = await pdfDoc.embedPng(pngImageBytes);
+      }
+
+      const document = data.employeeDocument.document;
+      var documentBytes;
+      var documentImage;
+      if (document) {
+        documentBytes = await fetch(document).then((res) => res.arrayBuffer());
+        documentImage = await pdfDoc.embedJpg(documentBytes);
+      }
+      var document2Image = null;
+      if (data.employeeDocument2) {
+        const document2 = data.employeeDocument2.document;
+        const document2Bytes = await fetch(document2).then((res) =>
+          res.arrayBuffer()
+        );
+        document2Image = await pdfDoc.embedJpg(document2Bytes);
+      }
+      const newPage = pdfDoc.addPage();
+
+      const pages = pdfDoc.getPages();
+      const firstPage = pages[0];
+
+      const { width, height } = firstPage.getSize();
+
+      const form = pdfDoc.getForm();
+
+      var pngDims;
+
+      if (pngImage) pngDims = pngImage.scale(0.1);
+
+      const lastname = form.getTextField(
+        "topmostSubform[0].Page1[0].Last_Name_Family_Name[0]"
+      );
+      const name = form.getTextField(
+        "topmostSubform[0].Page1[0].First_Name_Given_Name[0]"
+      );
+      const middle = form.getTextField(
+        "topmostSubform[0].Page1[0].Middle_Initial[0]"
+      );
+      const otherlastname = form.getTextField(
+        "topmostSubform[0].Page1[0].Other_Last_Names_Used_if_any[0]"
+      );
+      const address = form.getTextField(
+        "topmostSubform[0].Page1[0].Address_Street_Number_and_Name[0]"
+      );
+      const apt = form.getTextField("topmostSubform[0].Page1[0].Apt_Number[0]");
+      const city = form.getTextField(
+        "topmostSubform[0].Page1[0].City_or_Town[0]"
+      );
+      const zip = form.getTextField("topmostSubform[0].Page1[0].ZIP_Code[0]");
+      const birthday = form.getTextField(
+        "topmostSubform[0].Page1[0].Date_of_Birth_mmddyyyy[0]"
+      );
+      const ssn3 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__First_3_Numbers_[0]"
+      );
+      const ssn2 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__Next_2_numbers_[0]"
+      );
+      const ssn4 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__Last_4_numbers_[0]"
+      );
+      const email = form.getTextField(
+        "topmostSubform[0].Page1[0].Employees_Email_Address[0]"
+      );
+      const tel = form.getTextField(
+        "topmostSubform[0].Page1[0].Employees_Telephone_Number[0]"
+      );
+      const citizen = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._1\\._A_citizen_of_the_United_States[0]"
+      );
+      const noncitizen = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._2\\._A_noncitizen_national_of_the_United_States__See_instructions_[0]"
+      );
+      const resident = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._3\\._A_lawful_permanent_resident__Alien_Registration_Number_USCIS_Number__[0]"
+      );
+      const uscis = form.getTextField(
+        "topmostSubform[0].Page1[0].Alien_Registration_NumberUSCIS_Number_1[0]"
+      );
+      const alien = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._4\\._An_alien_authorized_to_work_until__expiration_date__if_applicable__mmd_dd_yyyy__[0]"
+      );
+      const exp = form.getTextField(
+        "topmostSubform[0].Page1[0].expiration_date__if_applicable__mm_dd_yyyy[0]"
+      );
+      const alienuscis = form.getTextField(
+        "topmostSubform[0].Page1[0]._1_Alien_Registration_NumberUSCIS_Number[0]"
+      );
+      const admision = form.getTextField(
+        "topmostSubform[0].Page1[0]._2_Form_I94_Admission_Number[0]"
+      );
+      const foreign = form.getTextField(
+        "topmostSubform[0].Page1[0]._3_Foreign_Passport_Number[0]"
+      );
+      const issuance = form.getTextField(
+        "topmostSubform[0].Page1[0].Country_of_Issuance[0]"
+      );
+      const nottranslator = form.getCheckBox(
+        "topmostSubform[0].Page1[0].I_did_not_use_a_preparer_or_translator[0]"
+      );
+      const translator = form.getCheckBox(
+        "topmostSubform[0].Page1[0].A_preparer_s__and_or_translator_s__assisted_the_employee_in_completing_Section_1[0]"
+      );
+      const lastnamet = form.getTextField(
+        "topmostSubform[0].Page1[0].Last_Name_Family_Name_2[0]"
+      );
+      const firstnamet = form.getTextField(
+        "topmostSubform[0].Page1[0].First_Name_Given_Name_2[0]"
+      );
+      const addresst = form.getTextField(
+        "topmostSubform[0].Page1[0].Address_Street_Number_and_Name_2[0]"
+      );
+      const cityt = form.getTextField(
+        "topmostSubform[0].Page1[0].City_or_Town_2[0]"
+      );
+      const zipcodet = form.getTextField(
+        "topmostSubform[0].Page1[0].Zip_Code[0]"
+      );
+      const statet = form.getDropdown("topmostSubform[0].Page1[0].State[0]");
+      const state2t = form.getDropdown("topmostSubform[0].Page1[0].State[1]");
+      const lastname2 = form.getTextField(
+        "topmostSubform[0].Page2[0].Last_Name_Family_Name_3[0]"
+      );
+      const firstname2 = form.getTextField(
+        "topmostSubform[0].Page2[0].First_Name_Given_Name_3[0]"
+      );
+      const middle2 = form.getTextField("topmostSubform[0].Page2[0].MI[0]");
+
+      lastname.setText(data.i9form.last_name);
+      name.setText(data.i9form.first_name);
+      middle.setText(data.i9form.middle_initial);
+      otherlastname.setText(data.i9form.other_last_name);
+      address.setText(data.i9form.address);
+      apt.setText(data.i9form.apt_number);
+      city.setText(data.i9form.city);
+      zip.setText(data.i9form.zipcode);
+      birthday.setText(data.i9form.date_of_birth);
+
+      if (data.i9form.social_security) {
+        var ssn = data.i9form.social_security.split("-");
+        ssn3.setText(ssn[0]);
+        ssn2.setText(ssn[1]);
+        ssn4.setText(ssn[2]);
+      }
+
+      email.setText(data.i9form.email);
+      tel.setText(data.i9form.phone);
+      if (data.i9form.employee_attestation === "CITIZEN") citizen.check();
+      else if (data.i9form.employee_attestation === "NON_CITIZEN")
+        noncitizen.check();
+      else if (data.i9form.employee_attestation === "ALIEN") alien.check();
+      else if (data.i9form.employee_attestation === "RESIDENT")
+        resident.check();
+
+      uscis.setText(data.i9form.USCIS);
+      exp.setText("");
+      alienuscis.setText(data.i9form.USCIS);
+      admision.setText(data.i9form.I_94);
+      foreign.setText(data.i9form.passport_number);
+      issuance.setText(data.i9form.country_issuance);
+
+      if (!data.i9form.translator) nottranslator.check();
+      else if (!data.i9form.translator) translator.check();
+
+      lastnamet.setText(data.i9form.translator_last_name);
+      firstnamet.setText(data.i9form.translator_first_name);
+      addresst.setText(data.i9form.translator_address);
+      cityt.setText(data.i9form.translator_city);
+      zipcodet.setText(data.i9form.translator_zipcode);
+      if (data.i9form.translator_state) {
+        statet.select(data.i9form.translator_state);
+        state2t.select(data.i9form.translator_state);
+      }
+      lastname2.setText(data.i9form.last_name);
+      firstname2.setText(data.i9form.first_name);
+      middle2.setText(data.i9form.middle_initial);
+
+      if (documentImage) {
+        pages[3].drawImage(documentImage, {
+          height: 325,
+          width: 275,
+          x: 50,
+          y: 790 - 325,
+        });
+      }
+
+      if (document2Image) {
+        pages[3].drawImage(document2Image, {
+          height: 325,
+          width: 275,
+          x: 50,
+          y: 790 - 325 - 350,
+        });
+      }
+      firstPage.drawText(moment(data.w4form.created_at).format("MM/DD/YYYY"), {
+        x: 375,
+        y: 257,
+        size: 10,
+        color: rgb(0, 0, 0),
+      });
+
+      if (pngImage) {
+        firstPage.drawImage(pngImage, {
+          x: 115,
+          y: 240,
+          width: pngDims.width,
+          height: pngDims.height,
+        });
+      }
+
+      const pdfBytes = await pdfDoc.save();
+      var blob = new Blob([pdfBytes], { type: "application/pdf" });
+      const fileURL = URL.createObjectURL(blob);
+      setForm(fileURL);
+      setFormLoading(false);
+      //   saveAs(blob, `${data.i9form.first_name + "_" + data.i9form.last_name+"_I9"+moment().format("MMDDYYYY")}.pdf`);
+    }
+  }
+
+  return (
+    <div className="p-1 listcontents">
+      <div
+        className="modal fade"
+        id="exampleModalCenter"
+        tabIndex="-1"
+        role="dialog"
+        aria-labelledby="exampleModalCenterTitle"
+        aria-hidden="true"
+      >
+        <div className="modal-dialog modal-dialog-centered" role="document">
+          <div className="modal-content">
+            {form ? (
+              <iframe
+                src={form}
+                style={{ width: "800px", height: "900px" }}
+                frameBorder="0"
+              ></iframe>
+            ) : (
+              <div className="spinner-border text-center mx-auto" role="status">
+                <span className="sr-only">Loading...</span>
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+      <p className="text-right">
+        {period.status != "OPEN" ? (
+          <div>
+            <Button
+              className="btn btn-info text-left mr-4"
+              onClick={() => {
+                update(
+                  "payroll-periods",
+                  Object.assign(period, { status: "OPEN" })
+                )
+                  .then((_payment) =>
+                    setPayments(
+                      payments.map((_pay) => {
+                        return {
+                          ..._pay,
+                          status: "APPROVED",
+                        };
+                      })
+                    )
+                  )
+                  .catch((e) => Notify.error(e.message || e));
+              }}
+            >
+              Undo Period
+            </Button>
+            <Button
+              className="btn btn-info"
+              onClick={() => history.push("/payroll/report/" + period.id)}
+            >
+              Take me to the Payroll Report
+            </Button>
+          </div>
+        ) : (
+          <Button
+            icon="plus"
+            size="small"
+            onClick={() => {
+              const isOpen = period.payments.find((p) => p.status === "NEW");
+              const thereIsAnotherNew = payments.find(
+                (item) => item.status === "NEW"
+              );
+
+              if (isOpen) return;
+              if (thereIsAnotherNew)
+                setPayments(
+                  payments.map((_pay) => {
+                    if (_pay.status !== "NEW") return _pay;
+                    else {
+                      return {
+                        ..._pay,
+                        payments: _pay.payments.filter(
+                          (p) => p.status == "NEW"
+                        ),
+                      };
+                    }
+                  })
+                );
+
+              setPayments(
+                period.payments.concat([
+                  Payment({
+                    status: "NEW",
+                    employee: { id: "new" },
+                  }).defaults(),
+                ])
+              );
+              bar.close();
+            }}
+          >
+            Add employee to timesheet
+          </Button>
+        )}
+      </p>
+      {groupedPayments.length == 0 ? (
+        <p>No clockins to review for this period</p>
+      ) : (
+        groupedPayments
+          .sort((a, b) =>
+            a.employee.id === "new"
+              ? -1
+              : b.employee.id === "new"
+              ? 1
+              : a.employee.user.last_name.toLowerCase() >
+                b.employee.user.last_name.toLowerCase()
+              ? 1
+              : -1
+          )
+          .map((pay) => {
+            const total_hours = pay.payments
+              .filter((p) => p.status === "APPROVED" || p.status === "PAID")
+              .reduce(
+                (total, { regular_hours, over_time, breaktime_minutes }) =>
+                  total + Number(regular_hours) + Number(over_time),
+                0
+              );
+            const total_amount = pay.payments
+              .filter((p) => p.status === "APPROVED" || p.status === "PAID")
+              .reduce(
+                (
+                  total,
+                  { regular_hours, over_time, hourly_rate, breaktime_minutes }
+                ) =>
+                  total +
+                  (Number(regular_hours) + Number(over_time)) *
+                    Number(hourly_rate),
+                0
+              );
+            return (
+              <table
+                key={pay.employee.id}
+                className="table table-striped payroll-summary"
+              >
+                <thead>
+                  <tr>
+                    <th>
+                      {!pay.employee || pay.employee.id === "new" ? (
+                        <SearchCatalogSelect
+                          onChange={(selection) => {
+                            const _alreadyExists = !Array.isArray(payments)
+                              ? false
+                              : payments.find(
+                                  (p) => p.employee.id === selection.value
+                                );
+                            if (_alreadyExists)
+                              Notify.error(
+                                `${selection.label} is already listed on this timesheet`
+                              );
+                            else
+                              GET("employees/" + selection.value)
+                                .then((emp) => {
+                                  setPayments(
+                                    payments.map((p) => {
+                                      if (p.employee.id != "new") return p;
+                                      else
+                                        return Payment({
+                                          status: "NEW",
+                                          employee: emp,
+                                        }).defaults();
+                                    })
+                                  );
+                                })
+                                .catch((e) => Notify.error(e.message || e));
+                          }}
+                          searchFunction={(search) =>
+                            new Promise((resolve, reject) =>
+                              GET("catalog/employees?full_name=" + search)
+                                .then((talents) =>
+                                  resolve(
+                                    [
+                                      {
+                                        label: `${
+                                          talents.length == 0
+                                            ? "No one found: "
+                                            : ""
+                                        }Invite "${search}" to jobcore`,
+                                        value: "invite_talent_to_jobcore",
+                                      },
+                                    ].concat(talents)
+                                  )
+                                )
+                                .catch((error) => reject(error))
+                            )
+                          }
+                        />
+                      ) : (
+                        <div className="row">
+                          <div className="col-12 pr-0">
+                            <EmployeeExtendedCard
+                              className="pr-2"
+                              employee={pay.employee}
+                              showFavlist={false}
+                              hoverEffect={false}
+                              showButtonsOnHover={false}
+                              onClick={() => null}
+                            />
+                          </div>
+                          {/* {
+                                            pay.employee.employment_verification_status === "APPROVED" && (
+                                            <div className="col-2 my-auto pl-0">
+                                                <i style={{fontSize:"16px", cursor:"pointer", color:'#000000'}}className="fas fa-file-alt" onClick={() => getEmployeeDocumet(pay)}></i>
+                                            </div>
+                                            )
+                                        } */}
+                        </div>
+                      )}
+                      <div className="row" style={{ marginLeft: "40px" }}>
+                        <div className="col-6 pr-0">
+                          {pay.employee.employment_verification_status ===
+                          "APPROVED" ? (
+                            <span
+                              style={{ cursor: "pointer" }}
+                              data-toggle="modal"
+                              data-target="#exampleModalCenter"
+                              onClick={() => {
+                                if (!formLoading) getEmployeeDocumet(pay, "w4");
+                              }}
+                            >
+                              <i
+                                style={{ fontSize: "16px", color: "#43A047" }}
+                                className="fas fa-file-alt mr-1"
+                              ></i>
+                              {!formLoading ? "W-4" : "Loading"}
+                            </span>
+                          ) : (
+                            <span className="text-muted" 
+                              style={{ cursor: "pointer" }}
+                              data-toggle="modal"
+                              data-target="#exampleModalCenter"
+                              onClick={() => {
+                                if (!formLoading) getEmployeeDocumet(pay, "w4");
+                              }}
+                            >
+                              <i className="fas fa-exclamation-circle text-danger mr-1"></i>
+                              W-4
+                            </span>
+                          )}
+                        </div>
+                        <div className="col-6">
+                          {pay.employee.employment_verification_status ===
+                          "APPROVED" ? (
+                            <span
+                              style={{ cursor: "pointer" }}
+                              data-toggle="modal"
+                              data-target="#exampleModalCenter"
+                              onClick={() => {
+                                if (!formLoading) getEmployeeDocumet(pay, "i9");
+                              }}
+                            >
+                              <i
+                                style={{ fontSize: "16px", color: "#43A047" }}
+                                className="fas fa-file-alt mr-1"
+                              ></i>
+                              {!formLoading ? "I-9" : "Loading"}
+                            </span>
+                          ) : (
+                            <span className="text-muted"
+                              style={{ cursor: "pointer" }}
+                              data-toggle="modal"
+                              data-target="#exampleModalCenter"
+                              onClick={() => {
+                                if (!formLoading) getEmployeeDocumet(pay, "i9");
+                              }}
+                            >
+                              <i className="fas fa-exclamation-circle text-danger mr-1"></i>
+                              I-9
+                            </span>
+                          )}
+                        </div>
+                      </div>
+                    </th>
+                    <th>In</th>
+                    <th>Out</th>
+                    <th>Total</th>
+                    <th>Break</th>
+                    <th>
+                      With <br /> Break
+                    </th>
+                    <th>Diff</th>
+                    <th style={{ minWidth: "80px" }}>Message</th>
+                    <th style={{ minWidth: "80px" }}></th>
+                  </tr>
+                </thead>
+                <tbody>
+                  {pay.payments
+                    .sort((a, b) => {
+                      if (a && b) {
+                        if (a.shift && b.shift) {
+                          if (a.shift.starting_at && b.shift.starting_at) {
+                            return moment(a.shift.starting_at).diff(
+                              b.shift.starting_at
+                            );
+                          } else return -1;
+                        }
+                      } else return -1;
+                    })
+                    .map((p) => (
+                      <PaymentRow
+                        key={p.id}
+                        payment={p}
+                        period={period}
+                        employee={pay.employee}
+                        readOnly={p.status !== "PENDING" && p.status !== "NEW"}
+                        onApprove={(payment) => {
+                          p.status !== "NEW"
+                            ? update("payment", {
+                                ...payment,
+                                status: "APPROVED",
+                                employee: p.employee.id || p.employee,
+                                shift: payment.shift
+                                  ? payment.shift.id
+                                  : p.shift.id,
+                                id: p.id,
+                              }).then((_payment) =>
+                                setPayments(
+                                  payments.map((_pay) =>
+                                    _pay.id !== p.id
+                                      ? _pay
+                                      : {
+                                          ..._pay,
+                                          status: "APPROVED",
+                                          breaktime_minutes:
+                                            payment.breaktime_minutes,
+                                          over_time: payment.over_time,
+                                          regular_hours: payment.regular_hours,
+                                        }
+                                  )
+                                )
+                              )
+                            : create("payment", {
+                                ...payment,
+                                status: "APPROVED",
+                                employee: p.employee.id || p.employee,
+                                shift: payment.shift
+                                  ? payment.shift.id
+                                  : p.shift.id,
+                                payroll_period: period.id,
+                                id: p.id,
+                              }).then((_payment) =>
+                                setPayments(
+                                  payments.map((_pay) =>
+                                    _pay.id !== payment.id
+                                      ? _pay
+                                      : {
+                                          ...payment,
+                                          status: "APPROVED",
+                                          employee: _pay.employee,
+                                          over_time: payment.over_time,
+                                          breaktime_minutes:
+                                            payment.breaktime_minutes,
+                                          hourly_rate:
+                                            payment.shift.minimum_hourly_rate,
+                                          shift: payment.shift,
+                                          regular_hours: payment.regular_hours,
+                                          id: p.id || _pay.id || _payment.id,
+                                        }
+                                  )
+                                )
+                              );
+                        }}
+                        onUndo={(payment) =>
+                          update("payment", {
+                            status: "PENDING",
+                            id: p.id,
+                          }).then((_payment) =>
+                            setPayments(
+                              payments.map((_pay) =>
+                                _pay.id !== payment.id
+                                  ? _pay
+                                  : { ..._pay, status: "PENDING" }
+                              )
+                            )
+                          )
+                        }
+                        onReject={(payment) => {
+                          if (p.id === undefined)
+                            setPayments(
+                              payments.filter(
+                                (_pay) => _pay.id !== undefined && _pay.id
+                              )
+                            );
+                          else
+                            update("payment", {
+                              id: p.id,
+                              status: "REJECTED",
+                            }).then((_payment) =>
+                              setPayments(
+                                payments.map((_pay) =>
+                                  _pay.id !== _payment.id
+                                    ? _pay
+                                    : { ..._pay, status: "REJECTED" }
+                                )
+                              )
+                            );
+                        }}
+                      />
+                    ))}
+                  <tr>
+                    <td colSpan={5}>
+                      {period.status === "OPEN" && pay.employee.id !== "new" && (
+                        <Button
+                          icon="plus"
+                          size="small"
+                          onClick={() => {
+                            setPayments(
+                              payments.concat([
+                                Payment({
+                                  status: "NEW",
+                                  employee: pay.employee,
+                                  regular_hours: "0.00",
+                                  over_time: "0.00",
+                                  hourly_rate: "0.00",
+                                }).defaults(),
+                              ])
+                            );
+                          }}
+                        >
+                          Add new clockin
+                        </Button>
+                      )}
+                    </td>
+                    <td colSpan={4} className="text-right">
+                      Total: {!isNaN(total_hours) ? total_hours.toFixed(2) : 0}{" "}
+                      hr
+                      {!isNaN(total_hours) &&
+                      Math.round(total_hours * 100) / 100 > 40 ? (
+                        <Tooltip
+                          placement="bottom"
+                          trigger={["hover"]}
+                          overlay={
+                            <small>
+                              This employee has{" "}
+                              {Math.round((total_hours - 40) * 100) / 100}hr
+                              overtime{" "}
+                            </small>
+                          }
+                        >
+                          <i className="fas fa-stopwatch text-danger fa-xs mr-2"></i>
+                        </Tooltip>
+                      ) : null}
+                      <small className="d-block">
+                        {!isNaN(total_amount) &&
+                        !isNaN(total_hours) &&
+                        Math.round(total_hours * 100) / 100 < 40
+                          ? "$" + Math.round(total_amount * 100) / 100
+                          : null}
+                      </small>
+                      {!isNaN(total_hours) &&
+                      Math.round(total_hours * 100) / 100 > 40 ? (
+                        <div>
+                          <small className="d-block">
+                            Reg: $
+                            {!isNaN(total_amount)
+                              ? Math.round(total_amount * 100) / 100
+                              : total_amount}
+                          </small>
+                          <small className="d-block">
+                            OT: $
+                            {(
+                              ((Math.round(total_amount * 100) /
+                                100 /
+                                (Math.round(total_hours * 100) / 100)) *
+                                0.5 *
+                                Math.round((total_hours - 40) * 100)) /
+                              100
+                            ).toFixed(2)}
+                          </small>
+                          <small className="d-block">
+                            Total: $
+                            {(
+                              ((Math.round(total_amount * 100) /
+                                100 /
+                                (Math.round(total_hours * 100) / 100)) *
+                                0.5 *
+                                Math.round((total_hours - 40) * 100)) /
+                                100 +
+                              Math.round(total_amount * 100) / 100
+                            ).toFixed(2)}
+                          </small>
+                        </div>
+                      ) : null}
+                    </td>
+                  </tr>
+                  {/* <tr> */}
+
+                  {/* </tr> */}
+                </tbody>
+              </table>
+            );
+          })
+      )}
+
+      <div className="btn-bar text-right">
+        {period.status === "OPEN" ? (
+          <button
+            type="button"
+            className="btn btn-primary"
+            onClick={() => {
+              const unapproved = [].concat.apply(
+                [],
+                payments.find((p) => p.status === "PENDING")
+              );
+
+              // const unapproved = [].concat.apply([], payments.map(p => p.payments)).find(p => p.status === "PENDING");
+
+              // if (unapproved) Notify.error("There are still some payments that need to be approved or rejected");
+              if (Array.isArray(unapproved) && unapproved.length > 0)
+                Notify.error(
+                  "There are still some payments that need to be approved or rejected"
+                );
+              else if (Array.isArray(payments) && payments.length === 0)
+                Notify.error("There are no clockins to review for this period");
+              // else {history.push('/payroll/rating/' + period.id);}
+              else
+                update(
+                  "payroll-periods",
+                  Object.assign(period, { status: "FINALIZED" })
+                )
+                  .then((res) => {
+                    if (res) {
+                      history.push("/payroll/report/" + period.id);
+                    }
+                  })
+                  .catch((e) => Notify.error(e.message || e));
+            }}
+          >
+            Finalize Period
+          </button>
+        ) : (
+          //    else history.push('/payroll/rating/' + period.id);
+          //                 }}>Finalize Period</button>
+          <Button
+            className="btn btn-success"
+            onClick={() => history.push("/payroll/report/" + period.id)}
+          >
+            Take me to the Payroll Report
+          </Button>
+        )}
+      </div>
+    </div>
+  );
+};
+
+PayrollPeriodDetails.propTypes = {
+  history: PropTypes.object.isRequired,
+  match: PropTypes.object.isRequired,
+};
+
+function createMapOptions(maps) {
+  // next props are exposed at maps
+  // "Animation", "ControlPosition", "MapTypeControlStyle", "MapTypeId",
+  // "NavigationControlStyle", "ScaleControlStyle", "StrokePosition", "SymbolPath", "ZoomControlStyle",
+  // "DirectionsStatus", "DirectionsTravelMode", "DirectionsUnitSystem", "DistanceMatrixStatus",
+  // "DistanceMatrixElementStatus", "ElevationStatus", "GeocoderLocationType", "GeocoderStatus", "KmlLayerStatus",
+  // "MaxZoomStatus", "StreetViewStatus", "TransitMode", "TransitRoutePreference", "TravelMode", "UnitSystem"
+  return {
+    zoomControlOptions: {
+      position: maps.ControlPosition.RIGHT_CENTER,
+      style: maps.ZoomControlStyle.SMALL,
+    },
+    zoomControl: false,
+    scaleControl: false,
+    fullscreenControl: false,
+    mapTypeControl: false,
+  };
+}
+const Marker = ({ text, className }) => (
+  <div className={className}>
+    <i className="fas fa-map-marker-alt fa-lg"></i>
+  </div>
+);
+Marker.propTypes = {
+  text: PropTypes.string,
+  className: PropTypes.string,
+};
+Marker.defaultProps = {
+  className: "",
+};
+
+const LatLongClockin = ({ clockin, children, isIn }) => {
+  if (!clockin) return null;
+  const lat = isIn ? clockin.latitude_in : clockin.latitude_out;
+  const lng = isIn ? clockin.longitude_in : clockin.longitude_out;
+  const distance = isIn
+    ? clockin.distance_in_miles
+    : clockin.distance_out_miles;
+  const time = isIn
+    ? clockin.started_at.format("LT")
+    : clockin.ended_at
+    ? clockin.ended_at.format("LT")
+    : "";
+
+  return (
+    <Tooltip
+      placement="right"
+      trigger={["hover"]}
+      overlay={
+        <div
+          style={{ width: "200px", height: "200px" }}
+          className="p-0 d-inline-block"
+        >
+          <GoogleMapReact
+            bootstrapURLKeys={{ key: process.env.GOOGLE_MAPS_WEB_KEY }}
+            defaultCenter={{ lat: 25.7617, lng: -80.1918 }}
+            width="100%"
+            height="100%"
+            center={{ lat, lng }}
+            options={createMapOptions}
+            defaultZoom={14}
+          >
+            <Marker lat={lat} lng={lng} text={"Jobcore"} />
+          </GoogleMapReact>
+          <p
+            className={`m-0 p-0 text-center ${
+              distance > 0.2 ? "text-danger" : ""
+            }`}
+          >
+            {distance} miles away @ {time}
+            <br />
+            <small>
+              [ {lat}, {lng} ]
+            </small>
+          </p>
+        </div>
+      }
+    >
+      {children}
+    </Tooltip>
+  );
+};
+
+LatLongClockin.propTypes = {
+  clockin: PropTypes.object.isRequired,
+  isIn: PropTypes.bool.isRequired,
+  children: PropTypes.node.isRequired,
+};
+LatLongClockin.defaultProps = {
+  clockin: null,
+  isIn: true,
+  children: null,
+};
+
+const PaymentRow = ({
+  payment,
+  employee,
+  onApprove,
+  onReject,
+  onUndo,
+  readOnly,
+  period,
+  onChange,
+  selection,
+}) => {
+  const { bar } = useContext(Theme.Context);
+  if (!employee || employee.id === "new")
+    return (
+      <p className="px-3 py-1">⬆ Search an employee from the list above...</p>
+    );
+
+  const [clockin, setClockin] = useState(
+    Clockin(payment.clockin).defaults().unserialize()
+  );
+  const [shift, setShift] = useState(
+    Shift(payment.shift).defaults().unserialize()
+  );
+  const [possibleShifts, setPossibleShifts] = useState(null);
+
+  const [breaktime, setBreaktime] = useState(payment.breaktime_minutes);
+
+  let shiftStartingTimeNoSeconds = moment(shift.starting_at).format(
+    "YYYY-MM-DDTHH:mm"
+  );
+  let shiftEndingTimeNoSeconds = moment(shift.ending_at).format(
+    "YYYY-MM-DDTHH:mm"
+  );
+  const approvedClockin = payment.approved_clockin_time
+    ? moment(payment.approved_clockin_time).startOf("minute")
+    : clockin.started_at
+    ? clockin.started_at
+    : shift.starting_at;
+  const approvedClockout = payment.approved_clockout_time
+    ? moment(payment.approved_clockout_time).startOf("minute")
+    : clockin.ended_at
+    ? clockin.ended_at
+    : shift.ending_at;
+  const [approvedTimes, setApprovedTimes] = useState({
+    in: approvedClockin,
+    out: approvedClockout,
+  });
+
+  const clockInDuration = moment.duration(
+    approvedTimes.out.diff(approvedTimes.in)
+  );
+
+  // const clockinHours = !clockInDuration ? 0 : clockin.shift || !readOnly ? Math.round(clockInDuration.asHours() * 100) / 100 : "-";
+  const clockinHours = Math.round(clockInDuration.asHours() * 100) / 100;
+  const shiftStartTime = shift.starting_at.format("LT");
+  const shiftEndTime = shift.ending_at.format("LT");
+  const shiftNextDay = shift.ending_at.isBefore(shift.starting_at);
+  const shiftDuration = moment.duration(
+    moment(shiftEndingTimeNoSeconds).diff(moment(shiftStartingTimeNoSeconds))
+  );
+  const plannedHours = Math.round(shiftDuration.asHours() * 100) / 100;
+
+  const clockInDurationAfterBreak = clockInDuration.subtract(
+    breaktime,
+    "minute"
+  );
+  const clockInTotalHoursAfterBreak = !clockInDurationAfterBreak
+    ? 0
+    : clockInDurationAfterBreak.asHours().toFixed(2);
+  const clockInTotalHoursAfterBreakCost = (
+    Number(shift.price.amount) * Number(clockInTotalHoursAfterBreak)
+  ).toFixed(2);
+  const diff =
+    Math.round(
+      (Number(clockInTotalHoursAfterBreak) - Number(plannedHours)) * 100
+    ) / 100;
+  // const overtime = clockInTotalHoursAfterBreak > 40 ? clockInTotalHoursAfterBreak - 40 : 0;
+
+  const lateClockin =
+    clockin &&
+    shift &&
+    clockin.started_at.diff(shift.starting_at, "minutes") >= 15
+      ? true
+      : false;
+  const lateClockout =
+    clockin && shift && clockin.ended_at.diff(shift.ending_at, "minutes") >= 30
+      ? true
+      : false;
+  const earlyClockin =
+    clockin &&
+    shift &&
+    clockin.started_at.diff(shift.starting_at, "minutes") <= -30
+      ? true
+      : false;
+  const earlyClockout =
+    clockin && shift && clockin.ended_at.diff(shift.ending_at, "minutes") <= -30
+      ? true
+      : false;
+
+  useEffect(() => {
+    let subs = null;
+    if (payment.status === "NEW") {
+      fetchTemporal(
+        `employers/me/shifts?start=${moment(period.starting_at).format(
+          "YYYY-MM-DD"
+        )}&end=${moment(period.ending_at).format("YYYY-MM-DD")}&employee=${
+          employee.id
+        }`,
+        "employee-expired-shifts"
+      ).then((_shifts) => {
+        const _posibleShifts = _shifts.map((s) => ({
+          label: "",
+          value: Shift(s).defaults().unserialize(),
+        }));
+        setPossibleShifts(_posibleShifts);
+      });
+      subs = store.subscribe("employee-expired-shifts", (_shifts) => {
+        const _posibleShifts = _shifts.map((s) => ({
+          label: "",
+          value: Shift(s).defaults().unserialize(),
+        }));
+        const possible = _posibleShifts.map((item) => {
+          const obj = Object.assign({}, item);
+          obj["value"]["starting_at"] = moment(
+            item.value.starting_at,
+            "YYYY-MM-DDTHH:mm"
+          ).local();
+          obj["value"]["ending_at"] = moment(
+            item.value.ending_at,
+            "YYYY-MM-DDTHH:mm"
+          ).local();
+          obj["value"]["position"] = item.value.position.label
+            ? { title: item.value.position.label, id: item.value.position.id }
+            : item.value.position;
+          return obj;
+        });
+        setPossibleShifts(_posibleShifts);
+      });
+    }
+    return () => {
+      if (subs) subs.unsubscribe();
+    };
+  }, []);
+
+  return (
+    <tr id={"paymemt" + payment.id}>
+      {payment.status === "NEW" ? (
+        <td>
+          <Select
+            className="select-shifts"
+            value={
+              !possibleShifts
+                ? { label: "Loading talent shifts", value: "loading" }
+                : { value: shift }
+            }
+            components={{
+              Option: ShiftOption,
+              SingleValue: ShiftOptionSelected({ multi: false }),
+            }}
+            onChange={(selectedOption) => {
+              const _shift = selectedOption.value;
+              if (_shift) {
+                if (_shift == "new_shift")
+                  bar.show({
+                    slug: "create_expired_shift",
+                    data: {
+                      employeesToAdd: [
+                        {
+                          label:
+                            employee.user.first_name +
+                            " " +
+                            employee.user.last_name,
+                          value: employee.id,
+                        },
+                      ],
+                      // Dates are in utc so I decided to change it to local time
+                      starting_at: moment(period.starting_at),
+                      ending_at: moment(period.starting_at).add(2, "hours"),
+                      period_starting: moment(period.starting_at),
+                      period_ending: moment(period.ending_at),
+                      shift: _shift,
+                      application_restriction: "SPECIFIC_PEOPLE",
+                    },
+                  });
+                else {
+                  setShift(_shift);
+                  setBreaktime(0);
+                }
+              }
+            }}
+            options={
+              possibleShifts
+                ? [
+                    {
+                      label: "Add a shift",
+                      value: "new_shift",
+                      component: EditOrAddExpiredShift,
+                    },
+                  ].concat(possibleShifts)
+                : [
+                    {
+                      label: "Add a shift",
+                      value: "new_shift",
+                      component: EditOrAddExpiredShift,
+                    },
+                  ]
+            }
+          ></Select>
+        </td>
+      ) : (
+        <td>
+          <div className="shift-details">
+            <p className="p-o m-0">
+              <strong className="shift-date">
+                {shift.starting_at.format("ddd, ll")}
+              </strong>
+            </p>
+            <small className="shift-position text-success">
+              {shift.position.title || shift.position.label}
+            </small>{" "}
+            @
+            <small className="shift-location text-primary">
+              {" "}
+              {shift.venue.title}
+            </small>
+          </div>
+          {
+            <div>
+              {typeof shift.price == "string" ? (
+                shift.price === "0.0" ? (
+                  ""
+                ) : (
+                  <small className="shift-price"> ${shift.price}</small>
+                )
+              ) : (
+                <small className="shift-price">
+                  {" "}
+                  {shift.price.currencySymbol || "$"}
+                  {shift.price.amount || shift.minimum_hourly_rate}
+                </small>
+              )}{" "}
+              {clockin && (
+                <div className="d-inline-block">
+                  {clockin.latitude_in > 0 && (
+                    <LatLongClockin isIn={true} clockin={clockin}>
+                      <small
+                        className={`pointer mr-2 ${
+                          clockin.distance_in_miles > 0.2 ? "text-danger" : ""
+                        }`}
+                      >
+                        <i className="fas fa-map-marker-alt"></i> In
+                      </small>
+                    </LatLongClockin>
+                  )}
+                  {clockin.latitude_out > 0 && (
+                    <LatLongClockin isIn={false} clockin={clockin}>
+                      <small
+                        className={`pointer ${
+                          clockin.distance_out_miles > 0.2 ? "text-danger" : ""
+                        }`}
+                      >
+                        <i className="fas fa-map-marker-alt"></i> Out
+                      </small>
+                    </LatLongClockin>
+                  )}
+                  {clockin.author != employee.user.profile.id ? (
+                    <Tooltip
+                      placement="bottom"
+                      trigger={["hover"]}
+                      overlay={<small>Clocked in by a supervisor</small>}
+                    >
+                      <i className="fas fa-user-cog text-danger ml-2"></i>
+                    </Tooltip>
+                  ) : !moment(payment.created_at).isSame(
+                      moment(payment.updated_at)
+                    ) && payment.status === "PENDING" ? (
+                    <Tooltip
+                      placement="bottom"
+                      trigger={["hover"]}
+                      overlay={<small>Previously updated by supervisor</small>}
+                    >
+                      <i className="fas fa-user-edit text-danger ml-2"></i>
+                    </Tooltip>
+                  ) : null}
+                </div>
+              )}
+            </div>
+          }
+        </td>
+      )}
+      <td className="time">
+        {readOnly ? (
+          <p>
+            {approvedTimes.in !== undefined && approvedTimes.in.format("LT")}
+          </p>
+        ) : (
+          <TimePicker
+            showSecond={false}
+            defaultValue={approvedTimes.in}
+            format={TIME_FORMAT}
+            onChange={(value) => {
+              if (value && value !== undefined) {
+                let ended_at = approvedTimes.out;
+                if (value.isAfter(ended_at))
+                  ended_at = moment(ended_at).add(1, "days");
+                if (value && value !== undefined)
+                  setApprovedTimes({
+                    ...approvedTimes,
+                    in: value,
+                    out: ended_at,
+                  });
+              }
+            }}
+            value={approvedTimes.in}
+            use12Hours
+          />
+        )}
+        <small>({shiftStartTime})</small>
+      </td>
+      <td className="time">
+        {readOnly ? (
+          <p>
+            {approvedTimes.out !== undefined && approvedTimes.out.format("LT")}
+          </p>
+        ) : (
+          <TimePicker
+            className={`${
+              clockin.automatically_closed ? "border border-danger" : ""
+            }`}
+            showSecond={false}
+            defaultValue={approvedTimes.out}
+            format={TIME_FORMAT}
+            onChange={(d1) => {
+              if (d1) {
+                const starting = approvedTimes.in;
+                let ended_at = moment(clockin.started_at).set({
+                  hour: d1.get("hour"),
+                  minute: d1.get("minute"),
+                });
+                if (starting.isAfter(ended_at))
+                  ended_at = moment(ended_at).add(1, "days");
+                if (ended_at && ended_at !== undefined)
+                  setApprovedTimes({ ...approvedTimes, out: ended_at });
+              }
+            }}
+            value={approvedTimes.out}
+            use12Hours
+          />
+        )}
+        <small>({shiftEndTime})</small>
+        {shiftNextDay && (
+          <Tooltip
+            placement="bottom"
+            trigger={["hover"]}
+            overlay={<small>This shift ended on the next day</small>}
+          >
+            <i className="fas fa-exclamation-triangle fa-xs mr-2"></i>
+          </Tooltip>
+        )}
+        {clockin.automatically_closed && (
+          <Tooltip
+            placement="bottom"
+            trigger={["hover"]}
+            overlay={<small>Automatically clocked out</small>}
+          >
+            <i className="fas fa-stopwatch text-danger fa-xs mr-2"></i>
+          </Tooltip>
+        )}
+      </td>
+      <td style={{ minWidth: "75px", maxWidth: "75px" }}>
+        <p className="mt-1" style={{ marginBottom: "7px" }}>
+          {clockinHours}
+        </p>
+        <small className="d-block my-0">(Plan: {plannedHours})</small>
+      </td>
+      {readOnly ? (
+        <td>{payment.breaktime_minutes} min</td>
+      ) : (
+        <td
+          style={{ minWidth: "75px", maxWidth: "75px" }}
+          className="text-center"
+        >
+          {
+            <input
+              type="number"
+              className="w-100 rounded"
+              onChange={(e) =>
+                e.target.value != ""
+                  ? setBreaktime(Math.abs(parseInt(e.target.value)))
+                  : setBreaktime(0)
+              }
+              value={breaktime}
+            />
+          }
+          <small>minutes</small>
+        </td>
+      )}
+      <td>
+        <p className="mt-1" style={{ marginBottom: "7px" }}>
+          {Number(clockInTotalHoursAfterBreak)}
+        </p>
+        <small className="d-block my-0">
+          (${clockInTotalHoursAfterBreakCost})
+        </small>
+      </td>
+      <td>{clockin.shift || !readOnly ? diff : "-"}</td>
+      <td>
+        {
+          <div>
+            {clockin && clockin.automatically_closed && (
+              <span style={{ display: "block" }}>{"Forgot to clockout"}</span>
+            )}
+            {earlyClockin && (
+              <span style={{ display: "block" }}>{"Early clockin"}</span>
+            )}
+            {earlyClockout && (
+              <span style={{ display: "block" }}>{"Early clockout"}</span>
+            )}
+            {lateClockin && (
+              <span style={{ display: "block" }}>{"Late clockin"}</span>
+            )}
+            {lateClockout && (
+              <span style={{ display: "block" }}>{"Late clockout"}</span>
+            )}
+            {clockin && clockin.distance_in_miles > 0.2 && (
+              <span style={{ display: "block" }}>
+                {"Did not clockin on-site"}
+              </span>
+            )}
+            {clockin && clockin.distance_out_miles > 0.2 && (
+              <span style={{ display: "block" }}>
+                {"Did not clockout on-site"}
+              </span>
+            )}
+            {Number(diff) > 0.3 && (
+              <span style={{ display: "block" }}>
+                {"Worked more than the planned hours"}
+              </span>
+            )}
+            {readOnly && payment.breaktime_minutes > 30 && (
+              <span style={{ display: "block" }}>{"Long break time"}</span>
+            )}
+            {!earlyClockin &&
+              !earlyClockout &&
+              !lateClockout &&
+              !lateClockin &&
+              !clockin.automatically_closed &&
+              clockin.distance_in_miles < 0.2 &&
+              clockin.distance_out_miles < 0.2 &&
+              Number(diff) < 0.3 &&
+              readOnly &&
+              payment.breaktime_minutes < 30 && (
+                <span style={{ display: "block" }}>{"-"}</span>
+              )}
+          </div>
+        }
+      </td>
+
+      {readOnly ? (
+        <td className="text-center">
+          {payment.status === "APPROVED" ? (
+            <span>
+              <i className="fas fa-check-circle"></i>
+            </span>
+          ) : payment.status === "REJECTED" ? (
+            <span>
+              <i className="fas fa-times-circle"></i>
+            </span>
+          ) : payment.status === "PAID" ? (
+            <p className="m-0 p-0">
+              <span className="badge">paid</span>
+            </p>
+          ) : null}
+          {period.status === "OPEN" &&
+            (payment.status === "APPROVED" ||
+              payment.status === "REJECTED") && (
+              <i
+                onClick={() => onUndo(payment)}
+                className="fas fa-undo ml-2 pointer"
+              ></i>
+            )}
+        </td>
+      ) : (
+        <td className="">
+          <Button
+            color="success"
+            size="small"
+            icon="check"
+            onClick={(value) => {
+              if (payment.status === "NEW") {
+                if (shift.id === undefined)
+                  Notify.error(
+                    "You need to specify a shift for all the new clockins"
+                  );
+                else
+                  onApprove({
+                    shift: shift,
+                    employee: employee,
+                    clockin: null,
+                    breaktime_minutes: breaktime,
+                    regular_hours:
+                      plannedHours > clockInTotalHoursAfterBreak ||
+                      plannedHours === 0
+                        ? clockInTotalHoursAfterBreak
+                        : plannedHours,
+                    over_time: diff < 0 ? 0 : diff,
+                    //
+                    approved_clockin_time: approvedTimes.in,
+                    approved_clockout_time: approvedTimes.out,
+                  });
+              } else
+                onApprove({
+                  breaktime_minutes: breaktime,
+                  regular_hours:
+                    plannedHours > clockInTotalHoursAfterBreak ||
+                    plannedHours === 0
+                      ? clockInTotalHoursAfterBreak
+                      : plannedHours,
+                  over_time: diff < 0 ? 0 : diff,
+                  shift: shift,
+                  approved_clockin_time: approvedTimes.in,
+                  approved_clockout_time: approvedTimes.out,
+                });
+            }}
+          />
+          <br />
+          <Button
+            className="mt-1"
+            color="danger"
+            size="small"
+            icon={payment.status === "NEW" ? "trash" : "times"}
+            onClick={(value) => onReject({ status: "REJECTED" })}
+          />
+        </td>
+      )}
+    </tr>
+  );
+};
+PaymentRow.propTypes = {
+  payment: PropTypes.object,
+  period: PropTypes.object,
+  employee: PropTypes.object,
+  readOnly: PropTypes.bool,
+  onApprove: PropTypes.func,
+  onReject: PropTypes.func,
+  onUndo: PropTypes.func,
+  shifts: PropTypes.array,
+  onChange: PropTypes.func,
+  selection: PropTypes.object,
+};
+PaymentRow.defaultProps = {
+  shifts: [],
+  period: null,
+};
+
+/**
+ * SelectTimesheet
+ */
+
+const filterClockins = (formChanges, formData, onChange) => {
+  onChange(Object.assign(formChanges, { employees: [], loading: true }));
+
+  const query = queryString.stringify({
+    starting_at: formChanges.starting_at
+      ? formChanges.starting_at.format("YYYY-MM-DD")
+      : null,
+    ending_at: formChanges.ending_at
+      ? formChanges.ending_at.format("YYYY-MM-DD")
+      : null,
+    shift: formData.shift ? formData.shift.id || formData.shift.id : "",
+  });
+  search(ENTITIY_NAME, "?" + query).then((data) =>
+    onChange({ employees: data, loading: false })
+  );
+};
+
+export const SelectTimesheet = ({
+  catalog,
+  formData,
+  onChange,
+  onSave,
+  onCancel,
+  history,
+}) => {
+  const { bar } = useContext(Theme.Context);
+  const employer = store.getState("current_employer");
+  const [noMorePeriods, setNoMorePeriods] = useState(false);
+  const [periods, setPeriods] = useState(formData.periods);
+  if (
+    !employer ||
+    !employer.payroll_configured ||
+    !moment.isMoment(employer.payroll_period_starting_time)
+  ) {
+    return (
+      <div className="text-center">
+        <p>Please setup your payroll settings first.</p>
+        <Button
+          color="success"
+          onClick={() => history.push("/payroll/settings")}
+        >
+          Setup Payroll Settings
+        </Button>
+      </div>
+    );
+  }
+  if (!periods) return "Loading...";
+  let note = null;
+  if (periods && periods.length > 0) {
+    const end = moment(periods[0].ending_at);
+    end.add(7, "days");
+    if (end.isBefore(TODAY()))
+      note = "Payroll was generated until " + end.format("L");
+  }
+  // eslint-disable-next-line no-console
+
+  const payments = periods.map((e) => e.payments);
+  // .filter((value, index, self) => self.indexOf(value) === index);
+  function totalEmployees(payments) {
+    if (payments) {
+      var employees = payments.map((e) => e.employee.id);
+      var uniqueEmployees = employees.filter(function (v, i) {
+        return i == employees.lastIndexOf(v);
+      });
+
+      return "Employees: " + uniqueEmployees.length + " | ";
+    } else return "";
+  }
+
+  return (
+    <div>
+      <div className="top-bar">
+        <Button
+          icon="sync"
+          color="primary"
+          size="small"
+          rounded={true}
+          onClick={() =>
+            processPendingPayrollPeriods().then((_periods) =>
+              onChange(setPeriods(periods.concat(_periods)))
+            )
+          }
+          note={note}
+          notePosition="left"
+        />
+      </div>
+      <div className="row mb-4">
+        <div className="col-12">
+          <h2 className="mt-1">Select a timesheet:</h2>
+          <ul
+            className="scroll"
+            style={{
+              maxHeight: "800px",
+              overflowY: "auto",
+              padding: "10px",
+              margin: "-10px",
+            }}
+          >
+            <div>
+              {periods.length === 0 && (
+                <p>
+                  No previous payroll periods have been found. Please try
+                  clicking the icon above.
+                </p>
+              )}
+              {periods.map((p) => (
+                <GenericCard
+                  key={p.id}
+                  hover={true}
+                  className="pr-2"
+                  onClick={() => history.push(`/payroll/period/${p.id}`)}
+                >
+                  <div className="avatar text-center pt-1 bg-transparent">
+                    {p.status === "FINALIZED" || p.status === "PAID" ? (
+                      <i className="fas fa-check-circle"></i>
+                    ) : p.status === "OPEN" ? (
+                      <i className="far fa-circle"></i>
+                    ) : (
+                      ""
+                    )}
+                  </div>
+                  From {moment(p.starting_at).format("MMM DD, YYYY")} to{" "}
+                  {moment(p.ending_at).format("MMM DD, YYYY")}
+                  <p className="my-0">
+                    <small
+                      className={`badge ${
+                        p.total_payments > 0 ? "badge-secondary" : "badge-info"
+                      }`}
+                    >
+                      {"Employees: " +
+                        p.employee_count +
+                        " | Payments: " +
+                        p.total_payments}{" "}
+                    </small>
+                  </p>
+                </GenericCard>
+              ))}
+              {!noMorePeriods &&
+              Array.isArray(periods) &&
+              periods.length > 0 ? (
+                <div className="row text-center w-100 mt-3">
+                  <div className="col">
+                    <Button
+                      onClick={() => {
+                        const PAGINATION_MONTHS_LENGTH = 1;
+                        searchMe(
+                          `payroll-periods`,
+                          `?end=${moment(
+                            periods[periods.length - 1]["ending_at"]
+                          )
+                            .subtract(1, "weeks")
+                            .format("YYYY-MM-DD")}&start=${moment(
+                            periods[periods.length - 1]["starting_at"]
+                          )
+                            .subtract(PAGINATION_MONTHS_LENGTH, "months")
+                            .format("YYYY-MM-DD")}`,
+                          formData.periods
+                        ).then((newPeriods) => {
+                          if (
+                            Array.isArray(newPeriods) &&
+                            newPeriods.length > 0 &&
+                            newPeriods.length > periods.length
+                          ) {
+                            setPeriods(newPeriods);
+                          } else setNoMorePeriods(true);
+                        });
+                      }}
+                    >
+                      Load More
+                    </Button>
+                  </div>
+                </div>
+              ) : null}
+            </div>
+          </ul>
+        </div>
+      </div>
+    </div>
+  );
+};
+SelectTimesheet.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  history: PropTypes.object.isRequired,
+  onChange: PropTypes.func,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+export const SelectShiftPeriod = ({
+  catalog,
+  formData,
+  onChange,
+  onSave,
+  onCancel,
+  history,
+}) => {
+  const { bar } = useContext(Theme.Context);
+
+  let note = null;
+  if (formData.periods.length > 0) {
+    const end = moment(formData.periods[0].ending_at);
+    end.add(7, "days");
+    if (end.isBefore(TODAY()))
+      note = "Payroll was generated until " + end.format("MM dd");
+  }
+  return (
+    <div>
+      <div className="top-bar">
+        <Button
+          icon="sync"
+          color="primary"
+          size="small"
+          rounded={true}
+          onClick={() => null}
+          note={note}
+          notePosition="left"
+        />
+      </div>
+      <div className="row">
+        <div className="col-12">
+          <div>
+            <h2 className="mt-1">Select a payment period:</h2>
+            <Select
+              className="select-shifts"
+              isMulti={false}
+              value={{
+                value: null,
+                label: `Select a payment period`,
+              }}
+              defaultValue={{
+                value: null,
+                label: `Select a payment period`,
+              }}
+              components={{ Option: ShiftOption, SingleValue: ShiftOption }}
+              onChange={(selectedOption) =>
+                searchMe("payment", `?period=${selectedOption.id}`).then(
+                  (payments) => {
+                    onChange({
+                      selectedPayments: payments,
+                      selectedPeriod: selectedOption,
+                    });
+                    history.push(`/payroll/period/${selectedOption.id}`);
+                  }
+                )
+              }
+              options={[
+                {
+                  value: null,
+                  label: `Select a payment period`,
+                },
+              ].concat(formData.periods)}
+            />
+          </div>
+        </div>
+        {formData &&
+        typeof formData.selectedPayments != "undefined" &&
+        formData.selectedPayments.length > 0 ? (
+          <div className="col-12 mt-3">
+            <ul>
+              {formData.selectedPayments.map((payment, i) => {
+                return (
+                  <EmployeeExtendedCard
+                    key={i}
+                    employee={payment.employee}
+                    showFavlist={false}
+                    showButtonsOnHover={false}
+                    onClick={() => {
+                      bar.show({
+                        to:
+                          `/payroll/period/${formData.selectedPeriod.id}?` +
+                          queryString.stringify({
+                            talent_id: payment.employee.id,
+                          }),
+                      });
+                    }}
+                  >
+                    {payment.status === "PENDING" ? (
+                      <span>
+                        {" "}
+                        pending{" "}
+                        <i className="fas fa-exclamation-triangle mr-2"></i>
+                      </span>
+                    ) : payment.status === "PAID" ? (
+                      <span>
+                        {" "}
+                        unpaid <i className="fas fa-dollar-sign mr-2"></i>
+                      </span>
+                    ) : (
+                      <i className="fas fa-check-circle mr-2"></i>
+                    )}
+                  </EmployeeExtendedCard>
+                );
+              })}
+            </ul>
+          </div>
+        ) : typeof formData.loading !== "undefined" && formData.loading ? (
+          <div className="col-12 mt-3 text-center">Loading...</div>
+        ) : (
+          <div className="col-12 mt-3 text-center">
+            No talents found for this period or shift
+          </div>
+        )}
+      </div>
+    </div>
+  );
+};
+SelectShiftPeriod.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  history: PropTypes.object.isRequired,
+  onChange: PropTypes.func,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+export class PayrollRating extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      ratings: [],
+      employer: store.getState("current_employer"),
+      payrollPeriods: [],
+      payments: [],
+      singlePayrollPeriod: null,
+      reviews: [],
+    };
+  }
+
+  componentDidMount() {
+    this.subscribe(store, "current_employer", (employer) => {
+      this.setState({ employer });
+    });
+
+    const payrollPeriods = store.getState("payroll-periods");
+    this.subscribe(store, "payroll-periods", (_payrollPeriods) => {
+      // this.updatePayrollPeriod(_payrollPeriods);
+      //if(!this.state.singlePayrollPeriod) this.getSinglePeriod(this.props.match.params.period_id, payrollPeriods);
+    });
+    // if (!payrollPeriods) {
+    //     this.getSinglePeriod(this.props.match.params.period_id, payrollPeriods);
+    // }
+    // else {
+    //     this.updatePayrollPeriod(payrollPeriods);
+    //     this.getSinglePeriod(this.props.match.params.period_id, payrollPeriods);
+
+    // }
+    if (this.props.match.params.period_id !== undefined)
+      fetchSingle("payroll-periods", this.props.match.params.period_id).then(
+        (_period) => {
+          this.defaultRatings(_period).then((res) =>
+            this.setState({
+              ratings: res,
+              singlePayrollPeriod: _period,
+              payments: _period.payments,
+            })
+          );
+        }
+      );
+
+    this.removeHistoryListener = this.props.history.listen((data) => {
+      const period = /\/payroll\/period\/(\d+)/gm;
+      const periodMatches = period.exec(data.pathname);
+      // const search = /\?talent_id=(\d+)/gm;
+      // const searchMatches = search.exec(data.search);
+      if (periodMatches) this.getSinglePeriod(periodMatches[1]);
+    });
+    return () => {
+      payrollPeriods.unsubscribe();
+    };
+  }
+
+  defaultRatings(singlePeriod) {
+    return new Promise((resolve, reject) => {
+      if (!singlePeriod) resolve(null);
+      const shiftList = singlePeriod.payments
+        .map((s) => s.shift.id)
+        .filter((v, i, s) => s.indexOf(v) === i)
+        .join(",");
+      searchMe("ratings", "?shifts=" + shiftList)
+        .then((previousRatings) => {
+          let ratings = {};
+          singlePeriod.payments.forEach((pay) => {
+            if (typeof ratings[pay.employee.id] === "undefined")
+              ratings[pay.employee.id] = {
+                employee: pay.employee,
+                shifts: [],
+                rating: null,
+                comments: "",
+              };
+            const hasPreviousShift = previousRatings.find(
+              (r) =>
+                r.shift &&
+                pay.shift &&
+                r.shift.id === pay.shift.id &&
+                r.employee === pay.employee.id
+            );
+            if (!hasPreviousShift && pay.shift)
+              ratings[pay.employee.id].shifts.push(pay.shift.id);
+          });
+
+          resolve(Object.values(ratings));
+        })
+        .catch((error) =>
+          Notify.error("There was an error fetching the ratings for the shift")
+        );
+    });
+  }
+
+  getSinglePeriod(periodId, payrollPeriods) {
+    if (typeof periodId !== "undefined") {
+      if (!payrollPeriods) fetchSingle("payroll-periods", periodId);
+      else {
+        const singlePayrollPeriod = payrollPeriods.find(
+          (pp) => pp.id == periodId
+        );
+        this.defaultRatings(singlePayrollPeriod).then((payments) =>
+          this.setState({ singlePayrollPeriod, payments })
+        );
+      }
+    }
+  }
+
+  updatePayrollPeriod(payrollPeriods) {
+    if (payrollPeriods == null) return;
+
+    let singlePayrollPeriod = null;
+    if (typeof this.props.match.params.period_id !== "undefined") {
+      singlePayrollPeriod = payrollPeriods.find(
+        (pp) => pp.id == this.props.match.params.period_id
+      );
+    }
+
+    this.defaultRatings(singlePayrollPeriod).then((payments) =>
+      this.setState({
+        payrollPeriods,
+        singlePayrollPeriod: singlePayrollPeriod || null,
+        payments,
+      })
+    );
+  }
+
+  render() {
+    if (!this.state.employer) return "Loading...";
+    else if (
+      !this.state.employer.payroll_configured ||
+      !moment.isMoment(this.state.employer.payroll_period_starting_time)
+    ) {
+      return (
+        <div className="p-1 listcontents text-center">
+          <h3>Please setup your payroll settings first.</h3>
+          <Button
+            color="success"
+            onClick={() => this.props.history.push("/payroll-settings")}
+          >
+            Setup Payroll Settings
+          </Button>
+        </div>
+      );
+    }
+
+    return (
+      <div className="p-1 listcontents">
+        {/* {this.state.singlePayrollPeriod && this.state.singlePayrollPeriod.status == "FINALIZED" &&
+                <Redirect from={'/payroll/rating/' + this.state.singlePayrollPeriod.id} to={'/payroll/report/' + this.state.singlePayrollPeriod.id} />
+            } */}
+        <Theme.Consumer>
+          {({ bar }) => (
+            <span>
+              {!this.state.ratings ? (
+                ""
+              ) : this.state.singlePayrollPeriod ? (
+                <div>
+                  <p className="text-left">
+                    <h2 className="mb-0">
+                      Please rate the talents for this period (optional):
+                    </h2>
+                    <h4 className="mt-0">
+                      {this.state.singlePayrollPeriod.label || ""}
+                    </h4>
+                  </p>
+                </div>
+              ) : (
+                <p>No payments to review for this period</p>
+              )}
+
+              {this.state.ratings.map((list, i) => {
+                if (list.employee)
+                  return (
+                    <div className="row list-card" key={i}>
+                      <div className="col-1 my-auto">
+                        <Avatar url={list.employee.user.profile.picture} />
+                      </div>
+                      <div className="col-3 my-auto">
+                        <span>
+                          {list.employee.user.first_name +
+                            " " +
+                            list.employee.user.last_name}
+                        </span>
+                      </div>
+                      <div className="col my-auto">
+                        <StarRating
+                          onClick={(e) => {
+                            let newRating = Object.assign({}, this.state);
+                            newRating.ratings[i].rating = e;
+                            this.setState({
+                              newRating,
+                            });
+                          }}
+                          onHover={() => null}
+                          direction="right"
+                          fractions={2}
+                          quiet={false}
+                          readonly={false}
+                          totalSymbols={5}
+                          value={list.rating}
+                          placeholderValue={0}
+                          placeholderRating={Number(0)}
+                          emptySymbol="far fa-star md"
+                          fullSymbol="fas fa-star"
+                          placeholderSymbol={"fas fa-star"}
+                        />
+                      </div>
+                      <div className="col-6 my-auto">
+                        <TextareaAutosize
+                          style={{ width: "100%" }}
+                          placeholder="Any additional comments?"
+                          value={list.comments}
+                          onChange={(e) => {
+                            let newComment = Object.assign({}, this.state);
+                            newComment.ratings[i].comments = e.target.value;
+                            this.setState({
+                              newComment,
+                            });
+                          }}
+                        />
+                      </div>
+                    </div>
+                  );
+              })}
+
+              <div className="btn-bar mt-3 pt-3">
+                <button
+                  type="button"
+                  className="btn btn-primary"
+                  onClick={() => {
+                    const unrated = this.state.ratings.find(
+                      (p) => p.rating == null && p.shifts.length > 0
+                    );
+                    const rated = [].concat.apply(
+                      [],
+                      this.state.ratings
+                        .filter((s) => s.shifts.length > 0 && s.rating)
+                        .map((p) => {
+                          if (p.shifts.length > 1) {
+                            return p.shifts.map((s) => ({
+                              employee: p.employee.id,
+                              shift: s,
+                              rating: p.rating,
+                              comments: p.comments,
+                            }));
+                          } else {
+                            return [
+                              {
+                                employee: p.employee.id,
+                                shift: p.shifts[0],
+                                rating: p.rating,
+                                comments: p.comments,
+                                // payment: p.id
+                              },
+                            ];
+                          }
+                        })
+                    );
+                    // if (unrated) Notify.error("There are still some employees that need to be rated");
+                    // else {
+                    create("ratings", rated).then((res) => {
+                      this.props.history.push(
+                        "/payroll/report/" + this.state.singlePayrollPeriod.id
+                      );
+                      // if (res)update('payroll-periods', Object.assign(this.state.singlePayrollPeriod, { status: 'FINALIZED' }));
+                    });
+                    // .then((resp) => { this.props.history.push('/payroll/report/' + this.state.singlePayrollPeriod.id); })
+                    // .catch(e => Notify.error(e.message || e));
+
+                    // }
+                  }}
+                >
+                  Take me to the Payroll Report
+                </button>
+              </div>
+            </span>
+          )}
+        </Theme.Consumer>
+      </div>
+    );
+  }
+}
+export class PayrollReport extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      employer: store.getState("current_employer"),
+      payrollPeriods: [],
+      payments: [],
+      paymentInfo: [],
+      singlePayrollPeriod: null,
+    };
+  }
+
+  componentDidMount() {
+    this.subscribe(store, "current_employer", (employer) => {
+      this.setState({ employer });
+    });
+    this.subscribe(store, "payroll-period-payments", (paymentInfo) => {
+      const payrollPaymentsWithDeductible = paymentInfo.payments.map((e, i) => {
+        var temp = Object.assign({}, e);
+        if (e.employee.w4_year == 2019 || !e.employee.w4_year) {
+          if (e.employee.filing_status == "SINGLE") {
+            let federalWithholding = 0;
+
+            if (Number(temp.earnings) < 73) federalWithholding = 0;
+            else if (Number(temp.earnings) > 73 && Number(temp.earnings) < 263)
+              federalWithholding = Math.round(
+                0 + (Number(temp.earnings) - 73) * 0.1
+              );
+            else if (Number(temp.earnings) > 263 && Number(temp.earnings) < 845)
+              federalWithholding = Math.round(
+                19.0 + (Number(temp.earnings) - 263) * 0.12
+              );
+            else if (
+              Number(temp.earnings) > 845 &&
+              Number(temp.earnings) < 1718
+            )
+              federalWithholding = Math.round(
+                88.84 + (Number(temp.earnings) - 845) * 0.22
+              );
+            else if (
+              Number(temp.earnings) > 1718 &&
+              Number(temp.earnings) < 3213
+            )
+              federalWithholding = Math.round(
+                280.9 + (Number(temp.earnings) - 1718) * 0.24
+              );
+            else if (
+              Number(temp.earnings) > 3213 &&
+              Number(temp.earnings) < 4061
+            )
+              federalWithholding = Math.round(
+                639.7 + (Number(temp.earnings) - 3213) * 0.32
+              );
+            else if (
+              Number(temp.earnings) > 4061 &&
+              Number(temp.earnings) < 10042
+            )
+              federalWithholding = Math.round(
+                911.06 + (Number(temp.earnings) - 4061) * 0.35
+              );
+            else if (Number(temp.earnings) > 10042)
+              federalWithholding = Math.round(
+                3004.41 + (Number(temp.earnings) - 10042) * 0.37
+              );
+            else federalWithholding = 0;
+
+            temp.deduction_list.push({
+              name: "Federal Withholding",
+              amount: federalWithholding,
+            });
+            temp["deductions"] =
+              Math.round((temp["deductions"] + federalWithholding) * 100) / 100;
+            temp["amount"] =
+              Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+          } else if (e.employee.filing_status == "MARRIED_JOINTLY") {
+            let federalWithholding = 0;
+
+            if (Number(temp.earnings) < 229) federalWithholding = 0;
+            else if (Number(temp.earnings) > 229 && Number(temp.earnings) < 609)
+              federalWithholding = Math.round(
+                0 + (Number(temp.earnings) - 229) * 0.1
+              );
+            else if (
+              Number(temp.earnings) > 609 &&
+              Number(temp.earnings) < 1772
+            )
+              federalWithholding = Math.round(
+                38.0 + (Number(temp.earnings) - 609) * 0.12
+              );
+            else if (
+              Number(temp.earnings) > 1772 &&
+              Number(temp.earnings) < 3518
+            )
+              federalWithholding = Math.round(
+                177.56 + (Number(temp.earnings) - 1772) * 0.22
+              );
+            else if (
+              Number(temp.earnings) > 3518 &&
+              Number(temp.earnings) < 6510
+            )
+              federalWithholding = Math.round(
+                561.68 + (Number(temp.earnings) - 3518) * 0.24
+              );
+            else if (
+              Number(temp.earnings) > 6510 &&
+              Number(temp.earnings) < 8204
+            )
+              federalWithholding = Math.round(
+                1279.76 + (Number(temp.earnings) - 6510) * 0.32
+              );
+            else if (
+              Number(temp.earnings) > 8204 &&
+              Number(temp.earnings) < 12191
+            )
+              federalWithholding = Math.round(
+                1,
+                821.84 + (Number(temp.earnings) - 8204) * 0.35
+              );
+            else if (Number(temp.earnings) > 12191)
+              federalWithholding = Math.round(
+                3217.29 + (Number(temp.earnings) - 12191) * 0.37
+              );
+            else federalWithholding = 0;
+
+            temp.deduction_list.push({
+              name: "Federal Withholding",
+              amount: federalWithholding,
+            });
+            temp["deductions"] =
+              Math.round((temp["deductions"] + federalWithholding) * 100) / 100;
+            temp["amount"] =
+              Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+          }
+        } else if (e.employee.w4_year == 2020) {
+          if (e.employee.filing_status == "MARRIED_JOINTLY") {
+            if (!e.step2c_checked) {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 477) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 477 &&
+                Number(temp.earnings) < 857
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 477) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 857 &&
+                Number(temp.earnings) < 2020
+              )
+                federalWithholding = Math.round(
+                  38.0 + (Number(temp.earnings) - 857) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 2020 &&
+                Number(temp.earnings) < 3766
+              )
+                federalWithholding = Math.round(
+                  177.56 + (Number(temp.earnings) - 2020) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 3766 &&
+                Number(temp.earnings) < 6758
+              )
+                federalWithholding = Math.round(
+                  561.68 + (Number(temp.earnings) - 3766) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 6758 &&
+                Number(temp.earnings) < 8452
+              )
+                federalWithholding = Math.round(
+                  1279.76 + (Number(temp.earnings) - 6758) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 8452 &&
+                Number(temp.earnings) < 12439
+              )
+                federalWithholding = Math.round(
+                  1821.84 + (Number(temp.earnings) - 8452) * 0.35
+                );
+              else if (Number(temp.earnings) > 12439)
+                federalWithholding = Math.round(
+                  3004.41 + (Number(temp.earnings) - 12439) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            } else {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 238) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 238 &&
+                Number(temp.earnings) < 428
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 238) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 428 &&
+                Number(temp.earnings) < 1010
+              )
+                federalWithholding = Math.round(
+                  19.0 + (Number(temp.earnings) - 428) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 1010 &&
+                Number(temp.earnings) < 1883
+              )
+                federalWithholding = Math.round(
+                  88.84 + (Number(temp.earnings) - 1010) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 1883 &&
+                Number(temp.earnings) < 3379
+              )
+                federalWithholding = Math.round(
+                  280.9 + (Number(temp.earnings) - 1883) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 3379 &&
+                Number(temp.earnings) < 4226
+              )
+                federalWithholding = Math.round(
+                  639.94 + (Number(temp.earnings) - 3379) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 4226 &&
+                Number(temp.earnings) < 6220
+              )
+                federalWithholding = Math.round(
+                  910.98 + (Number(temp.earnings) - 4226) * 0.35
+                );
+              else if (Number(temp.earnings) > 6220)
+                federalWithholding = Math.round(
+                  1608.88 + (Number(temp.earnings) - 6220) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            }
+          } else if (
+            e.employee.filing_status == "SINGLE" ||
+            e.employee.filing_status == "MARRIED_SEPARATELY"
+          ) {
+            if (e.step2c_checked) {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 119) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 119 &&
+                Number(temp.earnings) < 214
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 119) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 214 &&
+                Number(temp.earnings) < 505
+              )
+                federalWithholding = Math.round(
+                  9.5 + (Number(temp.earnings) - 214) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 505 &&
+                Number(temp.earnings) < 942
+              )
+                federalWithholding = Math.round(
+                  44.42 + (Number(temp.earnings) - 505) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 942 &&
+                Number(temp.earnings) < 1689
+              )
+                federalWithholding = Math.round(
+                  140.56 + (Number(temp.earnings) - 942) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 1689 &&
+                Number(temp.earnings) < 2113
+              )
+                federalWithholding = Math.round(
+                  319.84 + (Number(temp.earnings) - 1689) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 2113 &&
+                Number(temp.earnings) < 5104
+              )
+                federalWithholding = Math.round(
+                  455.52 + (Number(temp.earnings) - 2113) * 0.35
+                );
+              else if (Number(temp.earnings) > 5104)
+                federalWithholding = Math.round(
+                  1502.37 + (Number(temp.earnings) - 5104) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            } else {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 238) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 238 &&
+                Number(temp.earnings) < 428
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 238) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 428 &&
+                Number(temp.earnings) < 1010
+              )
+                federalWithholding = Math.round(
+                  19.0 + (Number(temp.earnings) - 428) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 1010 &&
+                Number(temp.earnings) < 1883
+              )
+                federalWithholding = Math.round(
+                  88.84 + (Number(temp.earnings) - 1010) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 1883 &&
+                Number(temp.earnings) < 3379
+              )
+                federalWithholding = Math.round(
+                  280.9 + (Number(temp.earnings) - 1883) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 3379 &&
+                Number(temp.earnings) < 4226
+              )
+                federalWithholding = Math.round(
+                  639.94 + (Number(temp.earnings) - 3379) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 4226 &&
+                Number(temp.earnings) < 10208
+              )
+                federalWithholding = Math.round(
+                  910.98 + (Number(temp.earnings) - 4226) * 0.35
+                );
+              else if (Number(temp.earnings) > 10208)
+                federalWithholding = Math.round(
+                  3004.68 + (Number(temp.earnings) - 10208) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            }
+          } else if (e.employee.filing_status == "HEAD") {
+            if (e.step2c_checked) {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 179) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 179 &&
+                Number(temp.earnings) < 315
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 179) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 315 &&
+                Number(temp.earnings) < 696
+              )
+                federalWithholding = Math.round(
+                  13.6 + (Number(temp.earnings) - 315) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 696 &&
+                Number(temp.earnings) < 1001
+              )
+                federalWithholding = Math.round(
+                  59.32 + (Number(temp.earnings) - 696) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 1001 &&
+                Number(temp.earnings) < 1750
+              )
+                federalWithholding = Math.round(
+                  126.42 + (Number(temp.earnings) - 1001) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 1750 &&
+                Number(temp.earnings) < 2173
+              )
+                federalWithholding = Math.round(
+                  306.18 + (Number(temp.earnings) - 1750) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 2173 &&
+                Number(temp.earnings) < 5164
+              )
+                federalWithholding = Math.round(
+                  441.54 + (Number(temp.earnings) - 2173) * 0.35
+                );
+              else if (Number(temp.earnings) > 5164)
+                federalWithholding = Math.round(
+                  1488.39 + (Number(temp.earnings) - 5164) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            } else {
+              let federalWithholding = 0;
+
+              if (Number(temp.earnings) < 359) federalWithholding = 0;
+              else if (
+                Number(temp.earnings) > 359 &&
+                Number(temp.earnings) < 630
+              )
+                federalWithholding = Math.round(
+                  0 + (Number(temp.earnings) - 359) * 0.1
+                );
+              else if (
+                Number(temp.earnings) > 630 &&
+                Number(temp.earnings) < 1391
+              )
+                federalWithholding = Math.round(
+                  27.0 + (Number(temp.earnings) - 630) * 0.12
+                );
+              else if (
+                Number(temp.earnings) > 1391 &&
+                Number(temp.earnings) < 2003
+              )
+                federalWithholding = Math.round(
+                  118.42 + (Number(temp.earnings) - 1391) * 0.22
+                );
+              else if (
+                Number(temp.earnings) > 2003 &&
+                Number(temp.earnings) < 3499
+              )
+                federalWithholding = Math.round(
+                  253.06 + (Number(temp.earnings) - 2003) * 0.24
+                );
+              else if (
+                Number(temp.earnings) > 3499 &&
+                Number(temp.earnings) < 4346
+              )
+                federalWithholding = Math.round(
+                  612.1 + (Number(temp.earnings) - 3499) * 0.32
+                );
+              else if (
+                Number(temp.earnings) > 4346 &&
+                Number(temp.earnings) < 10328
+              )
+                federalWithholding = Math.round(
+                  883.14 + (Number(temp.earnings) - 4346) * 0.35
+                );
+              else if (Number(temp.earnings) > 10328)
+                federalWithholding = Math.round(
+                  2976.84 + (Number(temp.earnings) - 10328) * 0.37
+                );
+              else federalWithholding = 0;
+              temp.deduction_list.push({
+                name: "Federal Withholding",
+                amount: federalWithholding,
+              });
+              temp["deductions"] =
+                Math.round((temp["deductions"] + federalWithholding) * 100) /
+                100;
+              temp["amount"] =
+                Math.round((temp["earnings"] - temp["deductions"]) * 100) / 100;
+            }
+          }
+        }
+        return temp;
+      });
+      let newPaymentInfo = paymentInfo;
+      newPaymentInfo["payments"] = payrollPaymentsWithDeductible;
+      this.setState({ paymentInfo });
+    });
+    this.subscribe(store, "employee-payment", () => {
+      fetchPeyrollPeriodPayments(this.state.singlePayrollPeriod.id);
+    });
+    const payrollPeriods = store.getState("payroll-periods");
+    this.subscribe(store, "payroll-periods", (_payrollPeriods) => {
+      console.log(_payrollPeriods);
+    });
+
+    this.updatePayrollPeriod(payrollPeriods);
+    this.getSinglePeriod(this.props.match.params.period_id, payrollPeriods);
+
+    this.removeHistoryListener = this.props.history.listen((data) => {
+      const period = /\/payroll\/period\/(\d+)/gm;
+      const periodMatches = period.exec(data.pathname);
+      // const search = /\?talent_id=(\d+)/gm;
+      // const searchMatches = search.exec(data.search);
+      if (periodMatches) this.getSinglePeriod(periodMatches[1]);
+    });
+  }
+
+  groupPayments(singlePeriod) {
+    if (!singlePeriod) return null;
+
+    let groupedPayments = {};
+
+    if (singlePeriod.payments) {
+      singlePeriod.payments.forEach((pay) => {
+        if (typeof groupedPayments[pay.employee.id] === "undefined") {
+          groupedPayments[pay.employee.id] = {
+            employee: pay.employee,
+            payments: [],
+          };
+        }
+        groupedPayments[pay.employee.id].payments.push(pay);
+      });
+    }
+
+    return Object.values(groupedPayments);
+  }
+
+  getSinglePeriod(periodId, payrollPeriods) {
+    if (typeof periodId !== "undefined") {
+      if (!payrollPeriods || !this.state.singlePayrollPeriod)
+        fetchSingle("payroll-periods", periodId).then((period) => {
+          this.setState(
+            {
+              singlePayrollPeriod: period,
+              payments: this.groupPayments(period),
+            },
+            () => {
+              fetchPeyrollPeriodPayments(this.state.singlePayrollPeriod.id);
+            }
+          );
+        });
+      else {
+        const singlePayrollPeriod = payrollPeriods.find(
+          (pp) => pp.id == periodId
+        );
+        this.setState(
+          {
+            singlePayrollPeriod,
+            payments: this.groupPayments(singlePayrollPeriod),
+          },
+          () => {
+            fetchPeyrollPeriodPayments(this.state.singlePayrollPeriod.id);
+          }
+        );
+      }
+    }
+  }
+
+  updatePayrollPeriod(payrollPeriods) {
+    if (payrollPeriods == null) return;
+
+    let singlePayrollPeriod = null;
+    if (typeof this.props.match.params.period_id !== "undefined") {
+      singlePayrollPeriod = payrollPeriods.find(
+        (pp) => pp.id == this.props.match.params.period_id
+      );
+    }
+
+    this.setState({
+      payrollPeriods,
+      singlePayrollPeriod: singlePayrollPeriod || null,
+      payments: this.groupPayments(singlePayrollPeriod),
+    });
+  }
+  async getEmployeeDocumet(emp, type) {
+    this.setState({
+      formLoading: true,
+      form: null,
+    });
+    const id = emp.employee.id;
+
+    const w4form = await GET("employers/me/" + "w4-form" + "/" + id);
+    const i9form = await GET("employers/me/" + "i9-form" + "/" + id);
+    const employeeDocument = await GET(
+      "employers/me/" + "employee-documents" + "/" + id
+    );
+
+    const data = {
+      w4form: w4form[0],
+      i9form: i9form[0],
+      employeeDocument: employeeDocument[0] || "",
+      employeeDocument2: employeeDocument[1] || "",
+    };
+
+    if (type === "w4") this.fillForm(data);
+    else if (type === "i9") this.fillFormI9(data);
+  }
+
+  async fillForm(data) {
+    if (data) {
+      const signature = data.w4form.employee_signature;
+      const png = `data:image/png;base64,${signature}`;
+      const formUrl =
+        "https://api.vercel.com/now/files/20f93230bb41a5571f15a12ca0db1d5b20dd9ce28ca9867d20ca45f6651cca0f/fw4.pdf";
+
+      const formPdfBytes = await fetch(formUrl).then((res) =>
+        res.arrayBuffer()
+      );
+      const pngUrl = png;
+
+      var pngImageBytes;
+      var pdfDoc = await PDFDocument.load(formPdfBytes);
+      var pngImage;
+      if (signature) {
+        pngImageBytes = await fetch(pngUrl).then((res) => res.arrayBuffer());
+
+        pngImage = await pdfDoc.embedPng(pngImageBytes);
+      }
+      const pages = pdfDoc.getPages();
+      const firstPage = pages[0];
+
+      const { width, height } = firstPage.getSize();
+
+      const form = pdfDoc.getForm();
+
+      var pngDims;
+      if (pngImage) pngDims = pngImage.scale(0.18);
+
+      const nameField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_01[0]"
+      );
+      const lastNameField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_02[0]"
+      );
+      const socialSecurityField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_05[0]"
+      );
+      const addressField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_03[0]"
+      );
+      const cityField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step1a[0].f1_04[0]"
+      );
+      const fillingFieldSingle = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[0]"
+      );
+      const fillingFieldMarried = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[1]"
+      );
+      const fillingFieldHead = form.getCheckBox(
+        "topmostSubform[0].Page1[0].c1_1[2]"
+      );
+      const multipleJobsField = form.getCheckBox(
+        "topmostSubform[0].Page1[0].Step2c[0].c1_2[0]"
+      );
+      const step3aField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step3_ReadOrder[0].f1_06[0]"
+      );
+      const step3bField = form.getTextField(
+        "topmostSubform[0].Page1[0].Step3_ReadOrder[0].f1_07[0]"
+      );
+      const step3cField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_08[0]"
+      );
+      const step4aField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_09[0]"
+      );
+      const step4bField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_10[0]"
+      );
+      const step4cField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_11[0]"
+      );
+      const employerField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_13[0]"
+      );
+      const employmentDateField = form.getTextField(
+        "topmostSubform[0].Page1[0].f1_14[0]"
+      );
+      const einField = form.getTextField("topmostSubform[0].Page1[0].f1_15[0]");
+
+      nameField.setText(data.i9form.first_name);
+      lastNameField.setText(data.i9form.last_name);
+      socialSecurityField.setText(data.i9form.social_security);
+      addressField.setText(data.i9form.address);
+      cityField.setText(data.i9form.city);
+
+      if (data.w4form.filing_status == "SINGLE") fillingFieldSingle.check();
+      else if (data.w4form.filing_status == "MARRIED")
+        fillingFieldMarried.check();
+      else if (data.w4form.filing_status == "HEAD") fillingFieldHead.check();
+
+      if (data.w4form.step2c) multipleJobsField.check();
+
+      step3aField.setText(data.w4form.dependant3b);
+      step3bField.setText(data.w4form.dependant3c);
+
+      if (data.w4form.dependant3b && data.w4form.dependant3c) {
+        var totalDependant =
+          +data.w4form.dependant3b + +data.w4form.dependant3c;
+        step3cField.setText(totalDependant.toString());
+      }
+
+      step4aField.setText(data.w4form.step4a);
+      step4bField.setText(data.w4form.step4b);
+      step4cField.setText(data.w4form.step4c);
+      employerField.setText(
+        "JobCore Inc, 270 Catalonia AveCoral Gables, FL 33134"
+      );
+      employmentDateField.setText(
+        moment(data.w4form.updated_at).format("MM/DD/YYYY")
+      );
+      einField.setText("83-1919066");
+
+      firstPage.drawText(moment(data.w4form.created_at).format("MM/DD/YYYY"), {
+        x: 470,
+        y: firstPage.getHeight() / 5.5,
+        size: 14,
+        color: rgb(0, 0, 0),
+      });
+
+      if (pngImage) {
+        firstPage.drawImage(pngImage, {
+          x: firstPage.getWidth() / 7 - pngDims.width / 2 + 75,
+          y: firstPage.getHeight() / 4.25 - pngDims.height,
+          width: pngDims.width,
+          height: pngDims.height,
+        });
+      }
+
+      const pdfBytes = await pdfDoc.save();
+      var blob = new Blob([pdfBytes], { type: "application/pdf" });
+
+      const fileURL = URL.createObjectURL(blob);
+      this.setState({
+        form: fileURL,
+        formLoading: false,
+      });
+
+      //   window.open(blob);
+      //   saveAs(blob, `${data.i9form.first_name + "_" + data.i9form.last_name+"_W4"}.pdf`);
+    }
+  }
+  async fillFormI9(data) {
+    if (data) {
+      const signature = data.i9form.employee_signature;
+      const png = `data:image/png;base64,${signature}`;
+      const formUrl =
+        "https://api.vercel.com/now/files/5032a373d2e112174680444f0aac149210e4ac4c3c3b55144913e319cfa72bd4/i92.pdf";
+      const formPdfBytes = await fetch(formUrl).then((res) =>
+        res.arrayBuffer()
+      );
+      
+      const pngUrl = png;
+      var pngImageBytes;
+      var pngImage;
+      const pdfDoc = await PDFDocument.load(formPdfBytes);
+      if (signature) {
+        pngImageBytes = await fetch(pngUrl).then((res) => res.arrayBuffer());
+        pngImage = await pdfDoc.embedPng(pngImageBytes);
+      }
+
+      const document = data.employeeDocument.document;
+      var documentBytes;
+      var documentImage;
+      if (document) {
+        documentBytes = await fetch(document).then((res) => res.arrayBuffer());
+        documentImage = await pdfDoc.embedJpg(documentBytes);
+      }
+      var document2Image = null;
+      if (data.employeeDocument2) {
+        const document2 = data.employeeDocument2.document;
+        const document2Bytes = await fetch(document2).then((res) =>
+          res.arrayBuffer()
+        );
+        document2Image = await pdfDoc.embedJpg(document2Bytes);
+      }
+      const newPage = pdfDoc.addPage();
+
+      const pages = pdfDoc.getPages();
+      const firstPage = pages[0];
+
+      const { width, height } = firstPage.getSize();
+
+      const form = pdfDoc.getForm();
+
+      var pngDims;
+
+      if (pngImage) pngDims = pngImage.scale(0.1);
+
+      const lastname = form.getTextField(
+        "topmostSubform[0].Page1[0].Last_Name_Family_Name[0]"
+      );
+      const name = form.getTextField(
+        "topmostSubform[0].Page1[0].First_Name_Given_Name[0]"
+      );
+      const middle = form.getTextField(
+        "topmostSubform[0].Page1[0].Middle_Initial[0]"
+      );
+      const otherlastname = form.getTextField(
+        "topmostSubform[0].Page1[0].Other_Last_Names_Used_if_any[0]"
+      );
+      const address = form.getTextField(
+        "topmostSubform[0].Page1[0].Address_Street_Number_and_Name[0]"
+      );
+      const apt = form.getTextField("topmostSubform[0].Page1[0].Apt_Number[0]");
+      const city = form.getTextField(
+        "topmostSubform[0].Page1[0].City_or_Town[0]"
+      );
+      const zip = form.getTextField("topmostSubform[0].Page1[0].ZIP_Code[0]");
+      const birthday = form.getTextField(
+        "topmostSubform[0].Page1[0].Date_of_Birth_mmddyyyy[0]"
+      );
+      const ssn3 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__First_3_Numbers_[0]"
+      );
+      const ssn2 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__Next_2_numbers_[0]"
+      );
+      const ssn4 = form.getTextField(
+        "topmostSubform[0].Page1[0].U\\.S\\._Social_Security_Number__Last_4_numbers_[0]"
+      );
+      const email = form.getTextField(
+        "topmostSubform[0].Page1[0].Employees_Email_Address[0]"
+      );
+      const tel = form.getTextField(
+        "topmostSubform[0].Page1[0].Employees_Telephone_Number[0]"
+      );
+      const citizen = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._1\\._A_citizen_of_the_United_States[0]"
+      );
+      const noncitizen = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._2\\._A_noncitizen_national_of_the_United_States__See_instructions_[0]"
+      );
+      const resident = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._3\\._A_lawful_permanent_resident__Alien_Registration_Number_USCIS_Number__[0]"
+      );
+      const uscis = form.getTextField(
+        "topmostSubform[0].Page1[0].Alien_Registration_NumberUSCIS_Number_1[0]"
+      );
+      const alien = form.getCheckBox(
+        "topmostSubform[0].Page1[0]._4\\._An_alien_authorized_to_work_until__expiration_date__if_applicable__mmd_dd_yyyy__[0]"
+      );
+      const exp = form.getTextField(
+        "topmostSubform[0].Page1[0].expiration_date__if_applicable__mm_dd_yyyy[0]"
+      );
+      const alienuscis = form.getTextField(
+        "topmostSubform[0].Page1[0]._1_Alien_Registration_NumberUSCIS_Number[0]"
+      );
+      const admision = form.getTextField(
+        "topmostSubform[0].Page1[0]._2_Form_I94_Admission_Number[0]"
+      );
+      const foreign = form.getTextField(
+        "topmostSubform[0].Page1[0]._3_Foreign_Passport_Number[0]"
+      );
+      const issuance = form.getTextField(
+        "topmostSubform[0].Page1[0].Country_of_Issuance[0]"
+      );
+      const nottranslator = form.getCheckBox(
+        "topmostSubform[0].Page1[0].I_did_not_use_a_preparer_or_translator[0]"
+      );
+      const translator = form.getCheckBox(
+        "topmostSubform[0].Page1[0].A_preparer_s__and_or_translator_s__assisted_the_employee_in_completing_Section_1[0]"
+      );
+      const lastnamet = form.getTextField(
+        "topmostSubform[0].Page1[0].Last_Name_Family_Name_2[0]"
+      );
+      const firstnamet = form.getTextField(
+        "topmostSubform[0].Page1[0].First_Name_Given_Name_2[0]"
+      );
+      const addresst = form.getTextField(
+        "topmostSubform[0].Page1[0].Address_Street_Number_and_Name_2[0]"
+      );
+      const cityt = form.getTextField(
+        "topmostSubform[0].Page1[0].City_or_Town_2[0]"
+      );
+      const zipcodet = form.getTextField(
+        "topmostSubform[0].Page1[0].Zip_Code[0]"
+      );
+      const statet = form.getDropdown("topmostSubform[0].Page1[0].State[0]");
+      const state2t = form.getDropdown("topmostSubform[0].Page1[0].State[1]");
+      const lastname2 = form.getTextField(
+        "topmostSubform[0].Page2[0].Last_Name_Family_Name_3[0]"
+      );
+      const firstname2 = form.getTextField(
+        "topmostSubform[0].Page2[0].First_Name_Given_Name_3[0]"
+      );
+      const middle2 = form.getTextField("topmostSubform[0].Page2[0].MI[0]");
+
+      lastname.setText(data.i9form.last_name);
+      name.setText(data.i9form.first_name);
+      middle.setText(data.i9form.middle_initial);
+      otherlastname.setText(data.i9form.other_last_name);
+      address.setText(data.i9form.address);
+      apt.setText(data.i9form.apt_number);
+      city.setText(data.i9form.city);
+      zip.setText(data.i9form.zipcode);
+      birthday.setText(data.i9form.date_of_birth);
+
+      if (data.i9form.social_security) {
+        var ssn = data.i9form.social_security.split("-");
+        ssn3.setText(ssn[0]);
+        ssn2.setText(ssn[1]);
+        ssn4.setText(ssn[2]);
+      }
+
+      email.setText(data.i9form.email);
+      tel.setText(data.i9form.phone);
+      if (data.i9form.employee_attestation === "CITIZEN") citizen.check();
+      else if (data.i9form.employee_attestation === "NON_CITIZEN")
+        noncitizen.check();
+      else if (data.i9form.employee_attestation === "ALIEN") alien.check();
+      else if (data.i9form.employee_attestation === "RESIDENT")
+        resident.check();
+
+      uscis.setText(data.i9form.USCIS);
+      exp.setText("");
+      alienuscis.setText(data.i9form.USCIS);
+      admision.setText(data.i9form.I_94);
+      foreign.setText(data.i9form.passport_number);
+      issuance.setText(data.i9form.country_issuance);
+
+      if (!data.i9form.translator) nottranslator.check();
+      else if (!data.i9form.translator) translator.check();
+
+      lastnamet.setText(data.i9form.translator_last_name);
+      firstnamet.setText(data.i9form.translator_first_name);
+      addresst.setText(data.i9form.translator_address);
+      cityt.setText(data.i9form.translator_city);
+      zipcodet.setText(data.i9form.translator_zipcode);
+      if (data.i9form.translator_state) {
+        statet.select(data.i9form.translator_state);
+        state2t.select(data.i9form.translator_state);
+      }
+      lastname2.setText(data.i9form.last_name);
+      firstname2.setText(data.i9form.first_name);
+      middle2.setText(data.i9form.middle_initial);
+
+      if (documentImage) {
+        pages[3].drawImage(documentImage, {
+          height: 325,
+          width: 275,
+          x: 50,
+          y: 790 - 325,
+        });
+      }
+
+      if (document2Image) {
+        pages[3].drawImage(document2Image, {
+          height: 325,
+          width: 275,
+          x: 50,
+          y: 790 - 325 - 350,
+        });
+      }
+      firstPage.drawText(moment(data.w4form.created_at).format("MM/DD/YYYY"), {
+        x: 375,
+        y: 257,
+        size: 10,
+        color: rgb(0, 0, 0),
+      });
+
+      if (pngImage) {
+        firstPage.drawImage(pngImage, {
+          x: 115,
+          y: 240,
+          width: pngDims.width,
+          height: pngDims.height,
+        });
+      }
+
+      const pdfBytes = await pdfDoc.save();
+      var blob = new Blob([pdfBytes], { type: "application/pdf" });
+      const fileURL = URL.createObjectURL(blob);
+
+      this.setState({
+        form: fileURL,
+        formLoading: false,
+      });
+      //   saveAs(blob, `${data.i9form.first_name + "_" + data.i9form.last_name+"_I9"+moment().format("MMDDYYYY")}.pdf`);
+    }
+  }
+  getDifferentHours(id) {
+    if (
+      id &&
+      this.state.payments &&
+      Array.isArray(this.state.payments) &&
+      this.state.payments.length > 0
+    ) {
+      const s = this.state.payments
+        .filter((e) => {
+          if (e.employee && e.employee.id === id) {
+            return e;
+          } else null;
+        })
+        .map((p) => p.payments)[0]
+        .map((z) => ({
+          position: z.shift.position.title,
+          hourly_rate: parseFloat(z.hourly_rate),
+          hours: parseFloat(z.over_time) + parseFloat(z.regular_hours),
+          status: z.status,
+        }));
+
+      const output = s
+        .filter((e) => e.status == "APPROVED")
+        .reduce((accumulator, cur) => {
+          let hr = cur.hourly_rate;
+          let found = accumulator.find((elem) => elem.hourly_rate === hr);
+          if (found) found.hours += cur.hours;
+          else accumulator.push(cur);
+          return accumulator;
+        }, []);
+
+      return (
+        <table
+          className="table table-sm table-bordered border-dark"
+          style={{ fontSize: "12px", border: "1px solid black" }}
+        >
+          <thead style={{ background: "transparent" }}>
+            <tr style={{ background: "transparent", fontSize: "14px" }}>
+              <th scope="col">Position</th>
+              <th scope="col">Hours</th>
+              <th scope="col">$/hr</th>
+            </tr>
+          </thead>
+          <tbody>
+            {output.map((e, i) => (
+              <tr key={i} style={{ background: "transparent" }}>
+                <td>{e.position}</td>
+                <td>{e.hours.toFixed(2)}</td>
+                <td>{"$" + e.hourly_rate.toFixed(2)}</td>
+              </tr>
+            ))}
+          </tbody>
+        </table>
+      );
+    }
+  }
+
+  render() {
+    if (!this.state.employer) return "Loading...";
+    else if (
+      !this.state.employer.payroll_configured ||
+      !moment.isMoment(this.state.employer.payroll_period_starting_time)
+    ) {
+      return (
+        <div className="p-1 listcontents text-center">
+          <h3>Please setup your payroll settings first.</h3>
+          <Button
+            color="success"
+            onClick={() => this.props.history.push("/payroll/settings")}
+          >
+            Setup Payroll Settings
+          </Button>
+        </div>
+      );
+    }
+    const payrollPeriodLabel = this.state.singlePayrollPeriod
+      ? `Payments From ${moment(
+          this.state.singlePayrollPeriod.starting_at
+        ).format("MM-D-YY h:mm A")} to ${moment(
+          this.state.singlePayrollPeriod.ending_at
+        ).format("MM-D-YY h:mm A")}`
+      : "";
+    //const allowLevels = (window.location.search != '');
+    const formLoading = this.state.formLoading;
+    const form = this.state.form;
+    return (
+      <div className="p-1" style={{ maxWidth: "1200px" }}>
+        <div
+          className="modal fade"
+          id="exampleModalCenter"
+          tabIndex="-1"
+          role="dialog"
+          aria-labelledby="exampleModalCenterTitle"
+          aria-hidden="true"
+        >
+          <div className="modal-dialog modal-dialog-centered" role="document">
+            <div className="modal-content">
+              {form ? (
+                <iframe
+                  src={form}
+                  style={{ width: "800px", height: "900px" }}
+                  frameBorder="0"
+                ></iframe>
+              ) : (
+                <div
+                  className="spinner-border text-center mx-auto"
+                  role="status"
+                >
+                  <span className="sr-only">Loading...</span>
+                </div>
+              )}
+            </div>
+          </div>
+        </div>
+        <Theme.Consumer>
+          {({ bar }) => (
+            <span>
+              {!this.state.paymentInfo ? (
+                ""
+              ) : this.state.paymentInfo.payments &&
+                this.state.paymentInfo.payments.length > 0 ? (
+                <div>
+                  <p className="text-right">
+                    <h2>{payrollPeriodLabel}</h2>
+                  </p>
+                  <div className="row mb-4 text-right">
+                    <div className="col text-left">
+                      <Button
+                        size="small"
+                        onClick={() => {
+                          // res => this.props.history.push('/payroll/period/' + period.id
+                          const period = this.state.singlePayrollPeriod;
+                          update(
+                            "payroll-periods",
+                            Object.assign(period, { status: "OPEN" })
+                          )
+                            .then((res) =>
+                              this.props.history.push(
+                                "/payroll/period/" + period.id
+                              )
+                            )
+                            .catch((e) => Notify.error(e.message || e));
+                        }}
+                      >
+                        Undo Period
+                      </Button>
+                    </div>
+
+                    <div className="col">
+                      <Button
+                        size="small"
+                        onClick={() =>
+                          this.props.history.push(
+                            "/payroll/period/" +
+                              this.state.singlePayrollPeriod.id
+                          )
+                        }
+                      >
+                        Review Timesheet
+                      </Button>
+                    </div>
+                    <PDFDownloadLink
+                      document={
+                        <PayrollPeriodReport
+                          employer={this.state.employer}
+                          payments={this.state.paymentInfo.payments}
+                          period={this.state.singlePayrollPeriod}
+                        />
+                      }
+                      fileName={
+                        "JobCore payments" + payrollPeriodLabel + ".pdf"
+                      }
+                    >
+                      {({ blob, url, loading, error }) =>
+                        loading ? (
+                          "Loading..."
+                        ) : (
+                          <div className="col">
+                            <Button color="success" size="small">
+                              Export to PDF
+                            </Button>
+                          </div>
+                        )
+                      }
+                    </PDFDownloadLink>
+                  </div>
+
+                  {/* {this.state.singlePayrollPeriod.status == "OPEN" &&
+                                    <Redirect from={'/payroll/report/' + this.state.singlePayrollPeriod.id} to={'/payroll/rating/' + this.state.singlePayrollPeriod.id} />
+                                } */}
+                  <table className="table table-striped payroll-summary text-center">
+                    <thead>
+                      <tr>
+                        <th scope="col" className="text-left">
+                          Staff
+                        </th>
+                        {/* <th scope="col">Regular Hrs</th> */}
+                        <th scope="col"></th>
+                        <th scope="col">Over Time</th>
+                        <th scope="col">Total Hrs</th>
+                        <th scope="col">Pay Rate</th>
+                        <th scope="col">Earnings</th>
+                        <th scope="col">Federal Withholding</th>
+                        <th scope="col">Social Security</th>
+                        <th scope="col">Medicare</th>
+                        {/* <th scope="col">Taxes</th> */}
+                        <th scope="col">Amount</th>
+                        <th scope="col"></th>
+                      </tr>
+                    </thead>
+                    <tbody>
+                      {this.state.paymentInfo.payments
+                        .sort((a, b) =>
+                          a.employee.last_name.toLowerCase() >
+                          b.employee.last_name.toLowerCase()
+                            ? 1
+                            : -1
+                        )
+                        .map((pay) => {
+                          const totalHour =
+                            Math.round(
+                              (Number(pay.regular_hours) +
+                                Number(pay.over_time)) *
+                                100
+                            ) / 100;
+                          var payRate;
+
+                          if (totalHour > 40) {
+                            payRate = (
+                              pay.earnings /
+                              (40 + (totalHour - 40) * 1.5)
+                            ).toFixed(2);
+                          } else
+                            payRate = (pay.earnings / totalHour).toFixed(2);
+                          return (
+                            <tr key={pay.employee.id}>
+                              <td className="text-left">
+                                {pay.employee.last_name},{" "}
+                                {pay.employee.first_name}
+                                <div className="row pt-1 pb-1">
+                                  <div className="col pr-0">
+                                    {pay.employee
+                                      .employment_verification_status ===
+                                    "APPROVED" ? (
+                                      <span
+                                        style={{ cursor: "pointer" }}
+                                        data-toggle="modal"
+                                        data-target="#exampleModalCenter"
+                                        onClick={() => {
+                                          if (!formLoading)
+                                            this.getEmployeeDocumet(pay, "w4");
+                                        }}
+                                      >
+                                        <i
+                                          style={{
+                                            fontSize: "16px",
+                                            color: "#43A047",
+                                          }}
+                                          className="fas fa-file-alt mr-1"
+                                        ></i>
+                                        {!formLoading ? "W-4" : "Loading"}
+                                      </span>
+                                    ) : (
+                                      <span className="text-muted">
+                                        <i className="fas fa-exclamation-circle text-danger mr-1"></i>
+                                        W-4
+                                      </span>
+                                    )}
+                                  </div>
+                                  <div className="col">
+                                    {pay.employee
+                                      .employment_verification_status ===
+                                    "APPROVED" ? (
+                                      <span
+                                        style={{ cursor: "pointer" }}
+                                        data-toggle="modal"
+                                        data-target="#exampleModalCenter"
+                                        onClick={() => {
+                                          if (!formLoading)
+                                            this.getEmployeeDocumet(pay, "i9");
+                                        }}
+                                      >
+                                        <i
+                                          style={{
+                                            fontSize: "16px",
+                                            color: "#43A047",
+                                          }}
+                                          className="fas fa-file-alt mr-1"
+                                        ></i>
+                                        {!formLoading ? "I-9" : "Loading"}
+                                      </span>
+                                    ) : (
+                                      <span className="text-muted">
+                                        <i className="fas fa-exclamation-circle text-danger mr-1"></i>
+                                        I-9
+                                      </span>
+                                    )}
+                                  </div>
+                                </div>
+                                <p className="m-0 p-0">
+                                  <span className="badge">
+                                    {pay.paid ? "paid" : "unpaid"}
+                                  </span>
+                                </p>
+                              </td>
+                              <td className="p-0">
+                                <div className="row">
+                                  <div className="col-12 pr-0 pl-0">
+                                    {this.getDifferentHours(pay.employee.id)}
+
+                                    {/* {Math.round((Number(pay.regular_hours) + Number(pay.over_time)) * 100) / 100 > 40 ? 40 : Math.round((Number(pay.regular_hours) + Number(pay.over_time)) * 100)/100} */}
+                                  </div>
+                                  {/* {this.getDifferentHours(pay.employee.id)} */}
+                                </div>
+                              </td>
+                              <td>
+                                {Math.round(
+                                  (Number(pay.regular_hours) +
+                                    Number(pay.over_time)) *
+                                    100
+                                ) /
+                                  100 >
+                                40
+                                  ? Math.round(
+                                      (Number(pay.regular_hours) +
+                                        Number(pay.over_time) -
+                                        40) *
+                                        100
+                                    ) / 100
+                                  : "-"}
+                              </td>
+                              <td>
+                                {Math.round(
+                                  (Number(pay.regular_hours) +
+                                    Number(pay.over_time)) *
+                                    100
+                                ) / 100}
+                              </td>
+                              <td>{"$" + Math.floor(payRate * 100) / 100}</td>
+                              <td>{pay.earnings}</td>
+                              <td>
+                                {pay.deduction_list.find(
+                                  (e) => e.name == "Federal Withholding"
+                                ).amount > 0
+                                  ? "-" +
+                                    pay.deduction_list.find(
+                                      (e) => e.name == "Federal Withholding"
+                                    ).amount
+                                  : 0}
+                              </td>
+                              <td>
+                                {"-" +
+                                  pay.deduction_list.find(
+                                    (e) => e.name == "Social Security"
+                                  ).amount}
+                              </td>
+                              <td>
+                                {"-" +
+                                  pay.deduction_list.find(
+                                    (e) => e.name == "Medicare"
+                                  ).amount}
+                              </td>
+                              {/* <td>{"-" + pay.deductions}</td> */}
+                              <td>{pay.amount}</td>
+                              <td>
+                                <Button
+                                  color="success"
+                                  size="small"
+                                  onClick={() =>
+                                    bar.show({
+                                      slug: "make_payment",
+                                      data: {
+                                        pay: pay,
+                                        paymentInfo: this.state.paymentInfo,
+                                        periodId:
+                                          this.state.singlePayrollPeriod.id,
+                                        bar: bar,
+                                      },
+                                    })
+                                  }
+                                >
+                                  {pay.paid
+                                    ? "Payment details"
+                                    : "Make payment"}
+                                </Button>
+                              </td>
+                              {/* <td>{Math.round((total.regular_hours + total.over_time) * 100) / 100}</td>
+                                                <td>${Math.round(total.total_amount * 100) / 100}</td> */}
+                            </tr>
+                          );
+                        })}
+                    </tbody>
+                  </table>
+                </div>
+              ) : (
+                <p>No payments to review for this period</p>
+              )}
+            </span>
+          )}
+        </Theme.Consumer>
+      </div>
+    );
+  }
+}
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_profile.js.html b/docs/views_profile.js.html new file mode 100644 index 0000000..d9383ee --- /dev/null +++ b/docs/views_profile.js.html @@ -0,0 +1,1043 @@ + + + + + JSDoc: Source: views/profile.js + + + + + + + + + + +
+ +

Source: views/profile.js

+ + + + + + +
+
+
import React, { useState, useContext, useEffect } from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import {
+  store,
+  fetchTemporal,
+  update,
+  updateProfileImage,
+  searchMe,
+  remove,
+  updateUser,
+  removeUser,
+  updateProfileMe,
+  sendCompanyInvitation,
+} from "../actions.js";
+import {
+  TIME_FORMAT,
+  DATETIME_FORMAT,
+  DATE_FORMAT,
+  TODAY,
+} from "../components/utils.js";
+import {
+  Button,
+  Theme,
+  GenericCard,
+  Avatar,
+  SearchCatalogSelect,
+  Wizard,
+} from "../components/index";
+import { Notify } from "bc-react-notifier";
+import { Session } from "bc-react-session";
+import { validator, ValidationError } from "../utils/validation";
+import Dropzone from "react-dropzone";
+import DateTime from "react-datetime";
+import moment from "moment";
+import PropTypes from "prop-types";
+import Select from "react-select";
+import { GET } from "../utils/api_wrapper";
+import { hasTutorial } from "../utils/tutorial";
+import Cropper from "react-cropper";
+import "cropperjs/dist/cropper.css";
+
+import Tooltip from "rc-tooltip";
+import "rc-tooltip/assets/bootstrap_white.css";
+import { select } from "underscore";
+
+export const Employer = (data = {}) => {
+  const _defaults = {
+    title: undefined,
+    website: undefined,
+    payroll_period_starting_time: TODAY(),
+    maximum_clockout_delay_minutes: 0,
+    bio: undefined,
+    uploadCompanyLogo: null,
+    editingImage: false,
+    response_time: undefined,
+    rating: undefined,
+    retroactive: undefined,
+    serialize: function () {
+      const newShift = {
+        //                status: (this.status == 'UNDEFINED') ? 'DRAFT' : this.status,
+      };
+
+      return Object.assign(this, newShift);
+    },
+  };
+
+  let _employer = Object.assign(_defaults, data);
+  return {
+    validate: () => {
+      if (_employer.bio && validator.isEmpty(_employer.bio))
+        throw new ValidationError("The company bio cannot be empty");
+      if (_employer.title && validator.isEmpty(_employer.title))
+        throw new ValidationError("The company name cannot be empty");
+      if (_employer.website && validator.isEmpty(_employer.website))
+        throw new ValidationError("The company website cannot be empty");
+      return _employer;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+  };
+};
+
+export class Profile extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      employer: Employer().defaults(),
+      currentUser: Session.getPayload().user.profile,
+      runTutorial: hasTutorial(),
+      stepIndex: 0,
+      steps: [
+        {
+          content: (
+            <div className="text-left">
+              <h1>Your Profile</h1>
+              <p>
+                Here you will edit your company information. You can also manage
+                subscription and view your company rating
+              </p>
+            </div>
+          ),
+          placement: "center",
+
+          styles: {
+            options: {
+              zIndex: 10000,
+            },
+            buttonClose: {
+              display: "none",
+            },
+          },
+          locale: { skip: "Skip tutorial" },
+          target: "body",
+        },
+        {
+          target: "#company-logo-circle",
+          content: "Click here to upload your company logo",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#company-logo-dropzone",
+          content: "Click the on the rectangle to upload a company logo",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#company-logo-save",
+          content: "Click save to update your logo",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#company_title",
+          content: "Edit company name",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#company_website",
+          content: "Edit company website",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#company_bio",
+          content: "Tell future employees about your business",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+        {
+          target: "#button_save",
+          content: "Save all your progress",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+
+        {
+          target: "#manage_locations",
+          content:
+            "Edit your company locations. This is how your employee will know where to report. You will need at least one company address in order to send shifts to future employees",
+          placement: "right",
+          styles: {
+            buttonClose: {
+              display: "none",
+            },
+            buttonNext: {
+              display: "none",
+            },
+          },
+          spotlightClicks: true,
+        },
+      ],
+    };
+  }
+
+  setEmployer(newEmployer) {
+    const employer = Object.assign(this.state.employer, newEmployer);
+    this.setState({ employer });
+  }
+
+  componentDidMount() {
+    const users = store.getState("users");
+    this.subscribe(store, "users", (_users) => {
+      const user = _users.filter(
+        (e) => e.profile.id == Session.getPayload().user.profile.id
+      )[0];
+      this.setState({ user: user });
+    });
+    if (users) {
+      const user = users.filter(
+        (e) => e.profile.id == Session.getPayload().user.profile.id
+      )[0];
+      this.setState({ user: user });
+    } else searchMe("users");
+
+    let employer = store.getState("current_employer");
+    if (employer) this.setState({ employer });
+    this.subscribe(store, "current_employer", (employer) => {
+      this.setState({ employer });
+    });
+  }
+
+  _crop() {
+    // image in dataUrl
+    console.log(this.cropper.getCroppedCanvas().toDataURL());
+  }
+
+  onCropperInit(cropper) {
+    this.cropper = cropper;
+  }
+  callback = (data) => {
+
+    // if(data.action == 'next' && data.index == 0){
+    //     this.props.history.push("/payroll");
+
+    // }
+    if (data.action == "next" && data.index == 0) {
+      this.setState({ stepIndex: 1 });
+    } else if (
+      data.action == "next" &&
+      data.index == 4 &&
+      data.lifecycle == "complete" &&
+      data.step.target == "#company_title"
+    ) {
+      this.setState({ stepIndex: 5 });
+    } else if (
+      data.action == "next" &&
+      data.index == 5 &&
+      data.lifecycle == "complete" &&
+      data.step.target == "#company_website"
+    ) {
+      this.setState({ stepIndex: 6 });
+    } else if (
+      data.action == "next" &&
+      data.index == 6 &&
+      data.lifecycle == "complete" &&
+      data.step.target == "#company_bio"
+    ) {
+      this.setState({ stepIndex: 7 });
+    }
+
+    if (data.action == "skip") {
+      const session = Session.get();
+      updateProfileMe({ show_tutorial: false });
+
+      const profile = Object.assign(session.payload.user.profile, {
+        show_tutorial: false,
+      });
+      const user = Object.assign(session.payload.user, { profile });
+      Session.setPayload({ user });
+      this.setState({ runTutorial: false });
+      document.getElementById("profilelink").style.backgroundColor = "";
+    }
+  };
+
+  render() {
+    return (
+      <div className="p-1 listcontents company-profile">
+        <Wizard
+          continuous
+          steps={this.state.steps}
+          run={this.state.runTutorial}
+          callback={(data) => this.callback(data)}
+          stepIndex={this.state.stepIndex}
+          allowClicksThruHole={true}
+          disableOverlay={true}
+          spotlightClicks={true}
+          styles={{
+            options: {
+              width: 600,
+              primaryColor: "#000",
+              zIndex: 1000,
+            },
+          }}
+        />
+        <h1>
+          <span id="company_details">User Details</span>
+        </h1>
+        <form>
+          <div className="row mt-2">
+            <div className="col-6">
+              <label>Name</label>
+              <p>
+                {this.state.user &&
+                  this.state.user.first_name + " " + this.state.user.last_name}
+              </p>
+            </div>
+            <div className="col-6">
+              <label>Email</label>
+              <p>{this.state.user && this.state.user.email}</p>
+            </div>
+          </div>
+        </form>
+        <h1>
+          <span id="company_details">Company Details</span>
+        </h1>
+        <form>
+          <div className="row mt-2">
+            <div className="col-6">
+              <label>Response Time</label>
+              <p>
+                You answer applications within{" "}
+                <span className="text-success">
+                  {this.state.employer.response_time} min.
+                </span>
+              </p>
+            </div>
+            <div className="col-6">
+              <label>Rating</label>
+              <p>
+                Talents rated you with{" "}
+                <span className="text-success">
+                  {this.state.employer.rating} points.
+                </span>
+              </p>
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12">
+              <label>Subscription</label>
+              <p>
+                {this.state.employer.active_subscription
+                  ? this.state.employer.active_subscription.title
+                  : "No active subscription"}
+                <Button
+                  className="ml-2"
+                  onClick={() =>
+                    this.props.history.push("/profile/subscription")
+                  }
+                  size="small"
+                >
+                  update
+                </Button>
+              </p>
+            </div>
+          </div>
+          <div className="row" id="company-logo-dropzone">
+            <div className="col-12">
+              <label>Company Logo</label>
+
+              {!this.state.editingImage ? (
+                <div
+                  id="company-logo-circle"
+                  className="company-logo"
+                  style={{
+                    backgroundImage: `url(${this.state.employer.picture})`,
+                  }}
+                >
+                  <Button
+                    color="primary"
+                    size="small"
+                    onClick={() =>
+                      this.setState({ editingImage: true, stepIndex: 2 })
+                    }
+                    icon="pencil"
+                  />
+                </div>
+              ) : (
+                <div>
+                  {this.state.uploadCompanyLogo ? (
+                    <div
+                      className="company-logo"
+                      style={{
+                        backgroundImage: `url(${URL.createObjectURL(
+                          this.state.uploadCompanyLogo
+                        )})`,
+                      }}
+                    >
+                      {" "}
+                      <Button
+                        color="primary"
+                        size="small"
+                        onClick={() =>
+                          this.setState({
+                            editingImage: false,
+                            uploadCompanyLogo: null,
+                          })
+                        }
+                        icon="times"
+                      />
+                    </div>
+                  ) : (
+                    <div>
+                      <Dropzone
+                        onDrop={(acceptedFiles) =>
+                          this.setState({
+                            uploadCompanyLogo: acceptedFiles[0],
+                            stepIndex: 3,
+                          })
+                        }
+                      >
+                        {({ getRootProps, getInputProps }) => {
+                          return (
+                            <section className="upload-zone">
+                              <div {...getRootProps()}>
+                                <input {...getInputProps()} />
+                                <strong
+                                  style={{
+                                    textDecoration: "underline",
+                                    cursor: "pointer",
+                                    fontSize: "20px",
+                                  }}
+                                >
+                                  Drop your company logo here, or click here to
+                                  open the file browser
+                                </strong>
+                              </div>
+                            </section>
+                          );
+                        }}
+                      </Dropzone>
+                    </div>
+                  )}
+
+                  <br />
+
+                  <Button
+                    className="mr-2"
+                    onClick={() =>
+                      this.setState({
+                        editingImage: false,
+                        uploadCompanyLogo: null,
+                      })
+                    }
+                    color="secondary"
+                  >
+                    Cancel
+                  </Button>
+                  <Button
+                    id="company-logo-save"
+                    onClick={() => {
+                      updateProfileImage(this.state.uploadCompanyLogo).then(
+                        (picture) => {
+                          this.setState((prevState) => {
+                            let employer = Object.assign(
+                              {},
+                              prevState.employer
+                            );
+                            employer.picture = picture;
+                            return {
+                              employer,
+                              editingImage: false,
+                              uploadCompanyLogo: null,
+                              stepIndex: 4,
+                            };
+                          });
+                        }
+                      );
+                    }}
+                    color="success"
+                  >
+                    Save
+                  </Button>
+                </div>
+              )}
+            </div>
+          </div>
+          <div className="row" id="company_title">
+            <div className="col-12">
+              <label>Company Name</label>
+              <input
+                type="text"
+                className="form-control"
+                value={this.state.employer.title}
+                onChange={(e) => this.setEmployer({ title: e.target.value })}
+              />
+            </div>
+          </div>
+          <div className="row mt-2" id="company_website">
+            <div className="col-12">
+              <label>Website</label>
+              <input
+                type="text"
+                className="form-control"
+                value={this.state.employer.website}
+                onChange={(e) => this.setEmployer({ website: e.target.value })}
+              />
+            </div>
+          </div>
+          <div className="row mt-2" id="company_bio">
+            <div className="col-12">
+              <label>Bio</label>
+              <input
+                type="text"
+                className="form-control"
+                value={this.state.employer.bio}
+                onChange={(e) => this.setEmployer({ bio: e.target.value })}
+              />
+            </div>
+          </div>
+          <div className="mt-4 text-right">
+            <button
+              type="button"
+              id="button_save"
+              className="btn btn-primary"
+              onClick={() => {
+                update(
+                  { path: "employers/me", event_name: "current_employer" },
+                  Employer(this.state.employer).validate().serialize()
+                ).catch((e) => Notify.error(e.message || e));
+                this.setState({ stepIndex: 8 });
+              }}
+            >
+              Save
+            </button>
+          </div>
+        </form>
+      </div>
+    );
+  }
+}
+Profile.propTypes = {
+  history: PropTypes.object,
+};
+
+export class ManageUsers extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      companyUsers: [],
+      currentUser: Session.getPayload().user.profile,
+    };
+  }
+
+  componentDidMount() {
+    const users = store.getState("users");
+    this.subscribe(store, "users", (_users) => {
+      this.setState({
+        companyUsers: _users,
+        currentUser: Session.getPayload().user.profile,
+      });
+    });
+    if (users)
+      this.setState({
+        companyUsers: users,
+        currentUser: Session.getPayload().user.profile,
+      });
+    else searchMe("users");
+
+    this.props.history.listen(() => {
+      this.filter();
+      this.setState({ firstSearch: false });
+    });
+  }
+
+  filter(users = null) {
+    searchMe("users", window.location.search);
+  }
+
+  showRole(profile) {
+    if (profile.employer.id === this.state.currentUser.employer.id) {
+      return profile.employer_role;
+    } else if (profile.employer.id != this.state.currentUser.employer.id) {
+      const role = profile.other_employers.find(
+        (emp) => emp.employer == this.state.currentUser.employer
+      );
+      if (role) return role.employer_role;
+      else return "";
+    } else return "";
+  }
+  render() {
+    const allowLevels = window.location.search != "";
+    return (
+      <div className="p-1 listcontents">
+        <Theme.Consumer>
+          {({ bar }) => (
+            <span>
+              <p className="text-right">
+                <h1 className="float-left">Company Users</h1>
+                <Button
+                  onClick={() =>
+                    bar.show({
+                      slug: "invite_user_to_employer",
+                      allowLevels: true,
+                    })
+                  }
+                >
+                  Invite new user
+                </Button>
+              </p>
+
+              {this.state.companyUsers.map((u, i) => (
+                <GenericCard key={i} hover={true}>
+                  <Avatar url={u.profile.picture} />
+                  <div className="btn-group">
+                    <Tooltip
+                      placement="bottom"
+                      trigger={["hover"]}
+                      overlay={
+                        <small>
+                          Admin can create shifts, make payroll payments and
+                          change employers role.
+                        </small>
+                      }
+                    >
+                      <Button
+                        color="primary"
+                        style={{ background: "white", color: "black" }}
+                        onClick={() => {
+                          if (this.state.currentUser.id === u.profile.id)
+                            Notify.error("You cannot make yourself an admin");
+                          else if (
+                            this.state.currentUser.employer_role != "ADMIN"
+                          ) {
+                            Notify.error(
+                              "You cannot change role if you are not ADMIN"
+                            );
+                          } else {
+                            const noti = Notify.info(
+                              "Are you sure you want to make this person Admin?",
+                              (answer) => {
+                                if (answer)
+                                  updateUser({
+                                    id: u.profile.id,
+                                    employer_id:
+                                      this.state.currentUser.employer.id,
+                                    employer_role: "ADMIN",
+                                  });
+                                noti.remove();
+                              }
+                            );
+                          }
+                        }}
+                      >
+                        Make Admin
+                      </Button>
+                    </Tooltip>
+
+                    <Tooltip
+                      placement="bottom"
+                      trigger={["hover"]}
+                      overlay={
+                        <small>
+                          Manager can create shifts and make payroll payments.
+                        </small>
+                      }
+                    >
+                      <Button
+                        style={{ background: "white", color: "black" }}
+                        onClick={() => {
+                          if (this.state.currentUser.id === u.profile.id)
+                            Notify.error("You cannot make yourself an manager");
+                          else if (
+                            this.state.currentUser.employer_role != "ADMIN"
+                          ) {
+                            Notify.error(
+                              "You cannot change role if you are not ADMIN"
+                            );
+                          } else {
+                            const noti = Notify.info(
+                              "Are you sure you want to make this person Manager?",
+                              (answer) => {
+                                if (answer)
+                                  updateUser({
+                                    id: u.profile.id,
+                                    employer_id:
+                                      this.state.currentUser.employer.id,
+                                    employer_role: "MANAGER",
+                                  });
+                                noti.remove();
+                              }
+                            );
+                          }
+                        }}
+                      >
+                        Make manager
+                      </Button>
+                    </Tooltip>
+
+                    <Tooltip
+                      placement="bottom"
+                      trigger={["hover"]}
+                      overlay={
+                        <small>
+                          Supervisor can create shifts and invite employees to
+                          work.
+                        </small>
+                      }
+                    >
+                      <Button
+                        style={{ background: "white", color: "black" }}
+                        onClick={() => {
+                          if (this.state.currentUser.id === u.profile.id)
+                            Notify.error(
+                              "You cannot make yourself an supervisor"
+                            );
+                          else if (
+                            this.state.currentUser.employer_role != "ADMIN"
+                          ) {
+                            Notify.error(
+                              "You cannot change role if you are not ADMIN"
+                            );
+                          } else {
+                            const noti = Notify.info(
+                              "Are you sure you want to make this person Supervisor?",
+                              (answer) => {
+                                if (answer)
+                                  updateUser({
+                                    id: u.profile.id,
+                                    employer_id:
+                                      this.state.currentUser.employer.id,
+                                    employer_role: "SUPERVISOR",
+                                  });
+                                noti.remove();
+                              }
+                            );
+                          }
+                        }}
+                      >
+                        Make Supervisor
+                      </Button>
+                    </Tooltip>
+                    <Button
+                      icon="trash"
+                      style={{ background: "white", color: "red" }}
+                      onClick={() => {
+                        if (this.state.currentUser.id === u.profile.id)
+                          Notify.error("You cannot delete yourself");
+                        else if (
+                          this.state.currentUser.employer_role != "ADMIN"
+                        ) {
+                          Notify.error(
+                            "You cannot delete if you are not ADMIN"
+                          );
+                        } else {
+                          const noti = Notify.info(
+                            "Are you sure you want to delete this user?",
+                            (answer) => {
+                              if (answer) removeUser(u);
+                              noti.remove();
+                            }
+                          );
+                        }
+                      }}
+                    ></Button>
+                  </div>
+                  <p className="mt-2">
+                    {u.first_name} {u.last_name} ({this.showRole(u.profile)})
+                  </p>
+                </GenericCard>
+              ))}
+            </span>
+          )}
+        </Theme.Consumer>
+      </div>
+    );
+  }
+}
+
+/**
+ * Invite a new user to the company
+ */
+export const InviteUserToCompanyJobcore = ({
+  onSave,
+  onCancel,
+  onChange,
+  catalog,
+  formData,
+}) => {
+  const { bar } = useContext(Theme.Context);
+  const [isNew, setIsNew] = useState(true);
+  const [selectedUser, setSelectedUser] = useState(null);
+  const [employer, setEmployer] = useState(Session.getPayload().user.profile);
+  if (selectedUser) formData.user = selectedUser.value;
+
+  return (
+    <form>
+      <div className="row">
+        <div className="col-12">
+          <p>
+            <span>Invite someone into your company </span>
+            <span
+              className="anchor"
+              onClick={() =>
+                bar.show({
+                  slug: "show_pending_jobcore_invites",
+                  allowLevels: true,
+                })
+              }
+            >
+              review previous invites
+            </span>
+            :
+          </p>
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12 align-content-center justify-content-center text-center mb-4">
+          <div className="btn-group btn-group-toggle" data-toggle="buttons">
+            <label className={"btn btn-secondary " + (isNew ? "active" : "")}>
+              <input
+                type="radio"
+                name="options"
+                id="option1"
+                autoComplete="off"
+                onClick={() => setIsNew(true)}
+                checked={isNew}
+              />{" "}
+              New User
+            </label>
+            <label className={"btn btn-secondary " + (!isNew ? "active" : "")}>
+              <input
+                type="radio"
+                name="options"
+                id="option2"
+                autoComplete="off"
+                onClick={() => setIsNew(false)}
+                checked={!isNew}
+              />{" "}
+              Existing User
+            </label>
+          </div>
+        </div>
+      </div>
+      {isNew ? (
+        <div>
+          <div className="row">
+            <div className="col-12">
+              <label>First Name</label>
+              <input
+                type="text"
+                className="form-control"
+                onChange={(e) => onChange({ first_name: e.target.value })}
+              />
+            </div>
+            <div className="col-12">
+              <label>Last Name</label>
+              <input
+                type="text"
+                className="form-control"
+                onChange={(e) => onChange({ last_name: e.target.value })}
+              />
+            </div>
+            <div className="col-12">
+              <label>Email</label>
+              <input
+                type="email"
+                className="form-control"
+                onChange={(e) => onChange({ email: e.target.value })}
+              />
+            </div>
+            <div className="col-12">
+              <label>Company Role</label>
+              <Select
+                value={catalog.employer_role.find(
+                  (a) => a.value == formData.employer_role
+                )}
+                onChange={(selection) =>
+                  onChange({ employer_role: selection.value.toString() })
+                }
+                options={catalog.employer_role}
+              />
+            </div>
+          </div>
+          <div className="btn-bar">
+            <Button color="success" onClick={() => onSave()}>
+              Send Invite
+            </Button>
+            <Button color="secondary" onClick={() => onCancel()}>
+              Cancel
+            </Button>
+          </div>
+        </div>
+      ) : (
+        <div>
+          <div className="row">
+            <div className="col-12">
+              <label>Search people in JobCore:</label>
+              <SearchCatalogSelect
+                isMulti={false}
+                value={selectedUser}
+                onChange={(selection) => {
+                  setSelectedUser({
+                    label: selection.label,
+                    value: selection.value,
+                  });
+                }}
+                searchFunction={(search) =>
+                  new Promise((resolve, reject) =>
+                    GET("catalog/profiles?full_name=" + search)
+                      .then((talents) =>
+                        resolve(
+                          [
+                            {
+                              label: `${
+                                talents.length == 0 ? "No one found: " : ""
+                              }Invite "${search}" to Company?`,
+                              value: "invite_talent_to_jobcore",
+                            },
+                          ].concat(talents)
+                        )
+                      )
+                      .catch((error) => reject(error))
+                  )
+                }
+              />
+            </div>
+
+            <div className="col-12">
+              <label>Company Role</label>
+              <Select
+                value={catalog.employer_role.find(
+                  (a) => a.value == formData.employer_role
+                )}
+                onChange={(selection) =>
+                  onChange({ employer_role: selection.value.toString() })
+                }
+                options={catalog.employer_role}
+              />
+            </div>
+          </div>
+          <div className="btn-bar">
+            <Button
+              color="success"
+              onClick={() => {
+                GET(`employers/me/users/${formData.user}`).then((user) =>
+                  sendCompanyInvitation(
+                    user.email,
+                    employer.employer,
+                    formData.employer_role,
+                    employer.id
+                  )
+                );
+              }}
+            >
+              Send Invite
+            </Button>
+            <Button color="secondary" onClick={() => onCancel()}>
+              Cancel
+            </Button>
+          </div>
+        </div>
+      )}
+    </form>
+  );
+};
+InviteUserToCompanyJobcore.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_ratings.js.html b/docs/views_ratings.js.html new file mode 100644 index 0000000..7bd345f --- /dev/null +++ b/docs/views_ratings.js.html @@ -0,0 +1,560 @@ + + + + + JSDoc: Source: views/ratings.js + + + + + + + + + + +
+ +

Source: views/ratings.js

+ + + + + + +
+
+
import React, { useContext, useState, useEffect } from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from 'prop-types';
+import { store, search, fetchTemporal, create, GET, searchMe } from '../actions.js';
+import { callback, hasTutorial } from '../utils/tutorial';
+import { GenericCard, Avatar, Stars, Theme, Button, Wizard, StarRating, SearchCatalogSelect, ShiftOption, ShiftOptionSelected, EmployeeExtendedCard } from '../components/index';
+import Select from 'react-select';
+import queryString from 'query-string';
+import { Session } from 'bc-react-session';
+import moment from 'moment';
+import { Notify } from 'bc-react-notifier';
+import { NOW } from "../components/utils.js";
+import { Talent } from '../views/talents.js';
+
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getRatingInitialFilters = (catalog) => {
+    let query = queryString.parse(window.location.search);
+    if (typeof query == 'undefined') return {};
+    if (!Array.isArray(query.positions)) query.positions = (typeof query.positions == 'undefined') ? [] : [query.positions];
+    if (!Array.isArray(query.badges)) query.badges = (typeof query.badges == 'undefined') ? [] : [query.badges];
+    return {
+        positions: query.positions.map(pId => catalog.positions.find(pos => pos.value == pId)),
+        badges: query.badges.map(bId => catalog.badges.find(b => b.value == bId)),
+        rating: catalog.stars.find(rate => rate.value == query.rating)
+    };
+};
+
+export const Rating = (data) => {
+
+    const session = Session.getPayload();
+    const _defaults = {
+        //foo: 'bar',
+        serialize: function () {
+
+            const newRating = {
+                comments: '',
+                rating: 5,
+                sender: null,
+                shift: null,
+                created_at: NOW()
+            };
+
+            return Object.assign(this, newRating);
+        },
+        unserialize: function () {
+            //this.fullName = function() { return (this.user.first_name.length>0) ? this.user.first_name + ' ' + this.user.last_name : 'No name specified'; };
+            let shift = null;
+            if (this.shift) {
+                shift = {
+                    ...this.shift,
+                    starting_at: moment(this.shift.starting_at),
+                    ending_at: moment(this.shift.ending_at)
+                };
+            }
+
+            return { ...this, shift };
+        }
+
+    };
+
+    let _entity = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+
+            return _entity;
+        },
+        defaults: () => {
+            return _defaults;
+        },
+        getFormData: () => {
+            const _formRating = {
+                id: _entity.id,
+                comments: _entity.comments,
+                rating: _entity.rating,
+                employee: _entity.employee,
+
+                // if more than one employee will be rated for the same shift
+                employees_to_rate: _entity ? _entity.employees : [],
+
+                sender: _entity.sender,
+                shift: _entity.shift ?
+                    {
+                        ..._entity.shift,
+                        starting_at: moment(_entity.shift.starting_at),
+                        ending_at: moment(_entity.shift.ending_at)
+                    }
+                    : null,
+                created_at: (moment.isMoment(_entity.created_at)) ? _entity.created_at : moment(_entity.created_at)
+            };
+            return _formRating;
+        },
+        filters: () => {
+            const _filters = {
+                //positions: _entity.positions.map( item => item.value ),
+            };
+            for (let key in _entity) if (typeof _entity[key] == 'function') delete _entity[key];
+            return Object.assign(_entity, _filters);
+        }
+    };
+};
+
+export class ManageRating extends Flux.DashView {
+
+    constructor() {
+        super();
+        this.state = {
+            ratings: [],
+            runTutorial: hasTutorial(),
+            steps: [],
+            employer: null
+        };
+    }
+
+    componentDidMount() {
+
+
+        // this.filter();
+   
+        this.props.history.listen(() => {
+            this.filter();
+            this.setState({ firstSearch: false });
+        });
+        this.setState({ runTutorial: true });
+
+        fetchTemporal('employers/me', 'current_employer');
+        this.subscribe(store, 'current_employer', (employer) => {
+            searchMe(`ratings`, `?employer=${employer.id}`);
+            this.setState({ employer });
+        });
+
+        const ratings = store.getState('ratings');
+        this.subscribe(store, 'ratings', (_ratings) => {
+            this.setState({ ratings:_ratings });
+        });
+    }
+
+    filter(ratings = null) {
+        search('ratings', `?employer=${this.state.employer.id}`);
+    }
+
+    render() {
+        console.log('state',this.state);
+        return (<div className="p-1 listcontents">
+            <Theme.Consumer>
+                {({ bar }) => (<span>
+                    <Wizard continuous
+                        steps={this.state.steps}
+                        run={this.state.runTutorial}
+                        callback={callback}
+                    />
+                    <h2>Company Ratings</h2>
+                    <div className="row mt-2">
+                        <div className="col-6">
+                            <label>Total Ratings</label>
+                            <p>You have been rated <span className="text-success">{this.state.ratings.length} times.</span></p>
+                        </div>
+                        <div className="col-6">
+                            <label>Rating</label>
+                            <p>Talents rated you with <span className="text-success">{this.state.employer ? this.state.employer.rating : 0} points avg.</span></p>
+                        </div>
+                    </div>
+                    <div className="row">
+                        <div className="col-12">
+                            <h3>Recent Ratings</h3>
+                            {this.state.ratings.filter(emp => !emp.employee && emp.shift).map((rate, i) => (
+                                <GenericCard key={i} hover={true} onClick={() => bar.show({ slug: "show_single_rating", data: rate, allowLevels: false })}>
+                                    <Avatar url={rate.sender.picture} />
+                                    <Stars className="float-left" rating={Number(rate.rating)} />
+                                    <span className="pl-1">{`on ${rate.created_at.substring(0, 10)}`}</span>
+                                    <p className="mt-0">{rate.comments !== '' ? `"${rate.comments}"` : `The talent didn't provide any comments for this rating.`}</p>
+                                </GenericCard>
+                            ))}
+                        </div>
+                    </div>
+                </span>)}
+            </Theme.Consumer>
+        </div>);
+    }
+}
+
+
+/**
+ * Talent Details
+ */
+export const RatingDetails = (props) => {
+    const { formData } = props;
+    const { shift } = formData;
+   
+    return (<Theme.Consumer>
+        {({ bar }) =>
+            (<li className="aplication-details">
+                <Avatar url={formData.sender.picture} />
+                <p>{formData.sender.user.first_name + ' ' + formData.sender.user.last_name}</p>
+                <div>
+                    <Stars rating={Number(formData.rating)} />
+                </div>
+                <h5 className="mt-3">
+                    {formData.comments !== '' ? `"${formData.comments}"` : `${formData.sender.user.first_name} didn't provide any comments for this rating.`}
+                </h5>
+                {!shift || typeof shift.position === 'undefined' ?
+                    'Loading shift information...' :
+                    <div>
+                        <a href="#" className="shift-position">{shift.position.title}</a> @
+                        <a href="#" className="shift-location"> {shift.venue.title}</a>
+                        <span className="shift-date"> {shift.starting_at.format('ll')} from {shift.starting_at.format('LT')} to {shift.ending_at.format('LT')} </span>
+                    </div>
+                }
+                <Button color="primary" onClick={() => bar.show({ slug: "review_single_talent", data: formData, allowLevels: false })}>Rate talent back</Button>
+            </li>)}
+    </Theme.Consumer>);
+};
+RatingDetails.propTypes = {
+    catalog: PropTypes.object.isRequired,
+    formData: PropTypes.object
+};
+
+
+/**
+ * Talent Details
+ */
+export const PendingRatings = (props) => {
+    return (<Theme.Consumer>
+        {({ bar }) =>
+            (<li className="aplication-details">
+            </li>)}
+    </Theme.Consumer>);
+};
+PendingRatings.propTypes = {
+    catalog: PropTypes.object.isRequired,
+    formData: PropTypes.object
+};
+
+
+/**
+ * Revire Talent for a specific shift
+ */
+export const ReviewTalentAndShift = (props) => {
+    const shift = props.formData.shift;
+    const employee = props.formData.employee;
+    const startDate = shift.starting_at.format('ll');
+    const startTime = shift.starting_at.format('LT');
+    const endTime = shift.ending_at.format('LT');
+    return (<Theme.Consumer>
+        {({ bar }) =>
+            (<li className="aplication-details">
+                <h4>How satisfied are you with {employee.user.first_name}{"'"}s performance during this shift?</h4>
+                <p className="mb-3">
+                    <Avatar url={employee.user.profile.picture} />
+                </p>
+                <a href="#" className="shift-position">{shift.position.title}</a> @
+                <a href="#" className="shift-location"> {shift.venue.title}</a>
+                <span className="shift-date"> {startDate} from {startTime} to {endTime} </span>
+                {
+                    (typeof shift.price == 'string') ?
+                        <span className="shift-price"> ${shift.price}</span>
+                        :
+                        <span className="shift-price"> {shift.price.currencySymbol}{shift.price.amount}</span>
+                }
+                <StarRating
+                    placeholderRating={Number(employee.rating ? employee.rating : 1)}
+                    emptySymbol="fa fa-star-o fa-2x"
+                    fullSymbol="fa fa-star fa-2x"
+                    placeholderSymbol={"fa fa-star fa-2x"}
+                />
+                <textarea className="form-control mt-3" placeholder={`Please describe further your experiences with ${employee.user.first_name}`}>
+                </textarea>
+                <div className="btn-bar">
+                    <Button color="secondary" onClick={() => bar.close()}>Cancel</Button>
+                    <Button color="primary" onClick={() => null}>Send</Button>
+                </div>
+            </li>)}
+    </Theme.Consumer>);
+};
+ReviewTalentAndShift.propTypes = {
+    catalog: PropTypes.object.isRequired,
+    formData: PropTypes.object
+};
+
+
+/**
+ * Review Talent in general
+ */
+export const RatingEmployees = (props) => {
+
+    const { onCancel, onSave, catalog, formData } = props;
+
+    const shiftEmployees = formData.shift.employees.map((e) => {
+        if (formData.ratings.find(rated => rated.employee == e.id)) {
+            var ratedEmployee = Object.assign({}, e);
+            ratedEmployee.rating = formData.ratings.find(rated => rated.employee == e.id).rating;
+            ratedEmployee.created_at = formData.ratings.find(rated => rated.employee == e.id).created_at;
+            return ratedEmployee;
+        } else {
+            return e;
+        }
+    });
+
+    return (<Theme.Consumer>
+        {({ bar }) => (<div className="sidebar-applicants">
+            {shiftEmployees.find(e => !e.rating) ? (
+                <div className="top-bar">
+                    <button type="button" className="btn btn-primary btn-sm"
+                        onClick={() => bar.show({ slug: "review_talent", data: { shift: formData.shift, employees: shiftEmployees.filter(e => !e.rating) }, allowLevels: true })}
+
+                    >
+                        Rate employee
+                    </button>
+                </div>
+            ) : (
+                    null
+                )}
+
+
+            <h3>Shift Ratings:</h3>
+            <ul style={{ overflowY: "auto", maxHeight: "75vh" }}>
+                {
+                    shiftEmployees.length > 0 ?
+                        shiftEmployees.map((tal, i) => (
+                            <GenericCard key={i} hover={true}>
+                                <Avatar url={tal.user.profile.picture} />
+                                <a href="#"><b>{tal.user.first_name + ' ' + tal.user.last_name}</b></a>
+
+                                <Stars rating={Number(tal.rating)} noRatingLabel="Not yet rated for this shift" />
+                                {
+                                    tal.rating ? null : (
+                                        <div className="btn-group" role="group" aria-label="Basic example">
+                                            <Button
+                                                className="mt-0 text-white" label="Rate"
+                                                notePosition="left" note="Rate Employee"
+                                                onClick={() => bar.show({ slug: "review_talent", data: { shift: formData.shift, employees: [tal] }, allowLevels: true })}
+                                            >
+                                                Rate
+                                            </Button>
+
+                                        </div>
+
+                                    )
+                                }
+                            </GenericCard>
+
+                        ))
+                        :
+                        <li>No ratings were found for this shift</li>
+                }
+            </ul>
+        </div>)}
+    </Theme.Consumer>);
+};
+RatingEmployees.propTypes = {
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    catalog: PropTypes.object, //contains the data needed for the form to load
+    formData: PropTypes.object, //contains the data needed for the form to load
+    context: PropTypes.object //contact any additional data for context purposes
+};
+export const UnratingEmployees = (props) => {
+
+    const { onCancel, onSave, catalog, formData } = props;
+    const unrated_employees = formData.employees.filter(e => !e.rating);
+    return (<Theme.Consumer>
+        {({ bar }) => (<div className="sidebar-applicants">
+
+            <div className="top-bar">
+                <button type="button" className="btn btn-primary btn-sm"
+                    onClick={() => bar.show({ slug: "review_talent", data: catalog.shift, allowLevels: true })}
+
+                >
+                    Rate employee
+                </button>
+            </div>
+
+            <h3>Shift Ratings:</h3>
+            <ul style={{ overflowY: "auto", maxHeight: "75vh" }}>
+                {
+                    unrated_employees.length > 0 ?
+                        unrated_employees.map((tal, i) => (
+                            <EmployeeExtendedCard
+                                key={i}
+                                employee={tal}
+                                hover={false}
+                                showFavlist={false}
+                            >
+
+                            </EmployeeExtendedCard>)
+                        )
+                        :
+                        <li>No ratings were found for this shift</li>
+                }
+            </ul>
+        </div>)}
+    </Theme.Consumer>);
+};
+UnratingEmployees.propTypes = {
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    catalog: PropTypes.object, //contains the data needed for the form to load
+    formData: PropTypes.object, //contains the data needed for the form to load
+    context: PropTypes.object //contact any additional data for context purposes
+};
+
+
+export const ReviewTalent = ({ onSave, onCancel, onChange, catalog, formData, error }) => {
+
+    console.log(formData);
+
+    const [shifts, setShifts] = useState([]);
+    const [rating, setRating] = useState(0);
+    const [comments, setComments] = useState('');
+    const [employeesToRate, setEmployeesToRate] = useState(formData.employees_to_rate.map(e => (
+        {
+            label: e.user.first_name + " " + e.user.last_name,
+            value: e.id
+        }
+    )));
+ 
+    return (<Theme.Consumer>
+        {({ bar }) => (
+            < form >
+                <div className="row">
+                    <div className="col-12">
+                        <label>Who worked on this shift?</label>
+
+                        <Select
+                            isMulti
+                            value={employeesToRate}
+                            onChange={(employees) => setEmployeesToRate(employees)}
+                            options={formData.employees_to_rate.map(e => ({
+                                label: e.user.first_name + " " + e.user.last_name,
+                                value: e.id
+                            }))}
+
+                        />
+
+                    </div>
+                </div>
+                {/* <div className="row">
+                    <div className="col-12">
+
+                        <label>What shift was it working?</label>
+                        <Select
+                            value={{ value: formData.shift }}
+                            components={{ Option: ShiftOption, SingleValue: ShiftOptionSelected({ multi: false }) }}
+                            onChange={(selection) => onChange({ shift: selection.value.toString() })}
+                            options={[]}
+                        />
+                    </div>
+                </div> */}
+                <div className="row">
+                    <div className="col-12">
+                        <label>How was his performance during the shift</label>
+                        <StarRating
+                            onClick={(e) =>
+                                setRating(e)
+                            }
+                            onHover={() => null}
+                            direction="right"
+                            fractions={2}
+                            quiet={false}
+                            readonly={false}
+                            totalSymbols={5}
+                            value={rating}
+                            placeholderValue={0}
+                            placeholderRating={Number(0)}
+                            emptySymbol="far fa-star md"
+                            fullSymbol="fas fa-star"
+                            placeholderSymbol={"fas fa-star"}
+                        />
+                    </div>
+                </div>
+                <div className="row">
+                    <div className="col-12">
+                        <label>Any comments?</label>
+                        <textarea className="form-control" onChange={e => setComments(e.target.value)}></textarea>
+                    </div>
+                </div>
+                <div className="btn-bar">
+                    <Button color="success"
+                        onClick={() => create('ratings',
+                            // {
+                            //     employee: formData.employees_to_rate.map(e => e.id),
+                            //     shifts: formData.shift.id,
+                            //     rating: rating,
+                            //     comments: comments
+                            // }
+                            formData.employees_to_rate.map(e => ({
+                                employee: e.id,
+                                shift: formData.shift.id,
+                                rating: rating,
+                                comments: comments
+                            }))
+                        ).then((res) => bar.close("last"))
+                            .catch(e => Notify.error(e.message || e))}>Send Review</Button>
+                </div>
+            </form>
+        )}
+    </Theme.Consumer>
+    );
+};
+ReviewTalent.propTypes = {
+    error: PropTypes.string,
+    bar: PropTypes.object,
+    onSave: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+    onChange: PropTypes.func.isRequired,
+    formData: PropTypes.object,
+    catalog: PropTypes.object //contains the data needed for the form to load
+};
+ReviewTalent.defaultProps = {
+    oldShift: null
+};
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_shifts.js.html b/docs/views_shifts.js.html new file mode 100644 index 0000000..d8c8700 --- /dev/null +++ b/docs/views_shifts.js.html @@ -0,0 +1,3261 @@ + + + + + JSDoc: Source: views/shifts.js + + + + + + + + + + +
+ +

Source: views/shifts.js

+ + + + + + +
+
+
import React, { useContext, useState, useEffect } from "react";
+import { Link } from "react-router-dom";
+import Flux from "@4geeksacademy/react-flux-dash";
+import {
+  store,
+  create,
+  searchMe,
+  fetchAllMe,
+  deleteShiftEmployee,
+} from "../actions.js";
+import PropTypes from "prop-types";
+import _ from "underscore";
+
+import Select from "react-select";
+
+import DateTime from "react-datetime";
+import TimePicker from "rc-time-picker";
+
+import { Notify } from "bc-react-notifier";
+import queryString from "query-string";
+import {
+  ShiftCard,
+  Wizard,
+  Theme,
+  SearchCatalogSelect,
+  Button,
+  ApplicantCard,
+  GenericCard,
+  EmployeeExtendedCard,
+  Avatar,
+} from "../components/index";
+import {
+  DATETIME_FORMAT,
+  TIME_FORMAT,
+  NOW,
+  TODAY,
+  YESTERDAY,
+} from "../components/utils.js";
+import { validator, ValidationError } from "../utils/validation";
+import { callback, hasTutorial } from "../utils/tutorial";
+import { AddOrEditLocation } from "../views/locations.js";
+import { ShiftInvite, Talent } from "../views/talents.js";
+import TextareaAutosize from "react-textarea-autosize";
+
+import moment from "moment";
+import { GET } from "../utils/api_wrapper";
+const SHIFT_POSSIBLE_STATUS = ["UNDEFINED", "DRAFT", "OPEN", "CANCELLED"];
+
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getShiftInitialFilters = () => {
+  return {};
+};
+
+export const Shift = (data) => {
+  const _defaults = {
+    position: "",
+    maximum_allowed_employees: "1",
+    application_restriction: "ANYONE",
+    minimum_hourly_rate: "8",
+    starting_at: moment().add(15 - (moment().minute() % 15), "minutes"),
+    ending_at: moment().add(15 - (moment().minute() % 15) + 120, "minutes"),
+    employees: [],
+    pending_invites: [],
+    pending_jobcore_invites: [],
+    candidates: [],
+    allowed_from_list: [],
+    allowedFavlists: [],
+    allowedTalents: [],
+    minimum_allowed_rating: "0",
+    venue: "",
+    status: "UNDEFINED",
+    withStatus: function (newStatus) {
+      if (typeof newStatus === "string") this.status = newStatus;
+      else throw new Error("Invalid status " + newStatus);
+
+      return this;
+    },
+    serialize: function () {
+      let newShift = {
+        status: this.status == "UNDEFINED" ? "DRAFT" : this.status,
+        // starting_at: (moment.isMoment(this.starting_at)) ? this.starting_at.format(DATETIME_FORMAT) : this.starting_at,
+        // ending_at: (moment.isMoment(this.ending_at)) ? this.ending_at.format(DATETIME_FORMAT) : this.ending_at,
+        starting_at: moment(this.starting_at),
+        ending_at: moment(this.ending_at),
+        allowed_from_list: this.allowedFavlists.map((f) => f.value),
+        multiple_dates:
+          Array.isArray(this.multiple_dates) && this.multiple_dates.length > 0
+            ? this.multiple_dates
+            : undefined,
+      };
+
+      // this is not ready yet
+      delete newShift.required_badges;
+      //this is a special property used on the form for creating an expried (past) shift and adding the employess right away
+      if (Array.isArray(this.employeesToAdd))
+        newShift.employees = this.employeesToAdd.map((e) => e.value || e.id);
+      return Object.assign(this, newShift);
+    },
+    unserialize: function () {
+      const dataType = typeof this.starting_at;
+      //if its already serialized
+      if (
+        typeof this.position == "object" &&
+        ["number", "string"].indexOf(dataType) == -1
+      )
+        return this;
+      const newShift = {
+        position:
+          typeof this.position != "object"
+            ? store.get("positions", this.position)
+            : this.position,
+        venue:
+          typeof this.venue != "object"
+            ? store.get("venues", this.venue)
+            : this.venue,
+        starting_at: !moment.isMoment(this.starting_at)
+          ? moment(this.starting_at)
+          : this.starting_at,
+        ending_at: !moment.isMoment(this.ending_at)
+          ? moment(this.ending_at)
+          : this.ending_at,
+        allowedFavlists: this.allowed_from_list.map((fav) => {
+          const list = store.get("favlists", fav.id || fav);
+          return list
+            ? { value: list.id, label: list.title, title: list.title }
+            : null;
+        }),
+        expired: moment(this.ending_at).isBefore(NOW()),
+        price: {
+          currency: "usd",
+          currencySymbol: "$",
+          amount: this.minimum_hourly_rate,
+          timeframe: "hr",
+        },
+      };
+      return Object.assign(this, newShift);
+    },
+  };
+
+  let _shift = Object.assign(_defaults, data);
+  return {
+    get: () => {
+      return _shift;
+    },
+    validate: () => {
+      const start = _shift.starting_at;
+      const finish = _shift.ending_at;
+
+      if (_shift.status == "CANCELLED") return _shift;
+
+      if (!validator.isInt(_shift.position.toString(), { min: 1 }))
+        throw new ValidationError("The shift is missing a position");
+      if (
+        !validator.isInt(_shift.maximum_allowed_employees.toString(), {
+          min: 1,
+          max: 25,
+        })
+      )
+        throw new ValidationError(
+          "The shift needs to employ at least 1 talent and no more than 25"
+        );
+      if (!validator.isFloat(_shift.minimum_hourly_rate.toString(), { min: 7 }))
+        throw new ValidationError("The minimum allowed hourly rate is $7");
+      // if (!start.isValid() || start.isBefore(NOW())) throw new ValidationError('The shift date has to be greater than today');
+      if (!finish.isValid() || finish.isBefore(start))
+        throw new ValidationError(
+          "The shift ending time has to be grater than the starting time"
+        );
+      if (!validator.isInt(_shift.venue.toString(), { min: 1 }))
+        throw new ValidationError("The shift is missing a venue");
+      if (SHIFT_POSSIBLE_STATUS.indexOf(_shift.status) == -1)
+        throw new Error('Invalid status "' + _shift.status + '" for shift');
+
+      return _shift;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _formShift = {
+        id: _shift.id.toString(),
+        pending_jobcore_invites: _shift.pending_jobcore_invites,
+        application_restriction: _shift.application_restriction,
+        pending_invites:
+          typeof _shift.pending_invites == "undefined"
+            ? []
+            : _shift.pending_invites,
+        position: _shift.position.id.toString() || _shift.position.toString(),
+        maximum_allowed_employees: _shift.maximum_allowed_employees.toString(),
+        minimum_hourly_rate: _shift.minimum_hourly_rate.toString(),
+        starting_at: _shift.starting_at,
+        ending_at: _shift.ending_at,
+        status: _shift.status,
+        allowedFavlists: _shift.allowedFavlists,
+        start_time: moment.isMoment(_shift.starting_at)
+          ? _shift.starting_at
+          : moment(_shift.starting_at + " " + _shift.starting_at).format(
+              "MM/DD/YYYY"
+            ),
+        finish_time: moment.isMoment(_shift.starting_at)
+          ? _shift.ending_at
+          : moment(_shift.ending_at + " " + _shift.ending_at).format(
+              "MM/DD/YYYY"
+            ),
+        minimum_allowed_rating: _shift.minimum_allowed_rating.toString(),
+        venue: _shift.venue.id.toString() || _shift.venue.toString(),
+        employees: _shift.employees,
+      };
+      return _formShift;
+    },
+  };
+};
+
+export class ManageShifts extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      shifts: [],
+      showNextButton: true,
+      offset: 10,
+      runTutorial: hasTutorial(),
+      steps: [
+        {
+          target: "#shift-details-header",
+          content: "Here you can see your entire list of shifts",
+          placement: "right",
+        },
+        {
+          target: "#create_shift",
+          content: "You can also create new shifts",
+          placement: "left",
+        },
+        {
+          target: "#filter_shift",
+          content: "Or filter them for better browsing",
+          placement: "left",
+        },
+      ],
+    };
+  }
+
+  componentDidMount() {
+    let status = queryString.parse(window.location.search, {
+      arrayFormat: "index",
+    });
+
+    // fetch if not loaded already
+    let shifts = store.getState("shifts");
+
+    if (status.status) {
+      searchMe(
+        `shifts`,
+        `?envelope=true&limit=10&${
+          status.status == "FILLED"
+            ? "filled=true&upcoming=true&not_status=DRAFT"
+            : "status=" + status.status
+        }`
+      ).then((data) => {
+        const showNextButton = data.next !== null;
+        this.setState({ showNextButton });
+      });
+    } else {
+      searchMe(`shifts`, `?envelope=true&limit=10`).then((data) => {
+        const showNextButton = data.next !== null;
+        this.setState({ showNextButton });
+      });
+    }
+    this.subscribe(store, "shifts", (shifts) => {
+      this.filterShifts(shifts);
+    });
+
+    this.props.history.listen(() => {
+      this.filterShifts();
+    });
+    this.setState({ runTutorial: true });
+  }
+
+  filterShifts(shifts = null) {
+    let filters = this.getFilters();
+    if (!shifts) shifts = store.getState("shifts");
+    if (Array.isArray(shifts) && shifts.length > 0)
+      this.setState({ shifts: shifts });
+    else this.setState({ shifts: [] });
+  }
+
+  getFilters() {
+    let filters = queryString.parse(window.location.search, {
+      arrayFormat: "index",
+    });
+    for (let f in filters) {
+      switch (f) {
+        case "status":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              let values = filters.status.value.split(",");
+              if (values.length == 1) {
+                if (values.includes("OPEN")) {
+                  if (
+                    moment(shift.ending_at).isBefore(NOW()) || //has passed
+                    shift.maximum_allowed_employees <= shift.employees.length //or its filled
+                  ) {
+                    return false;
+                  }
+                } else if (values.includes("FILLED")) {
+                  if (shift.maximum_allowed_employees > shift.employees.length)
+                    return false;
+                } else if (values.includes("EXPIRED")) {
+                  if (moment(shift.ending_at).isAfter(NOW())) return false;
+                  else values.push("OPEN");
+                }
+              }
+              return values.includes(shift.status);
+            },
+          };
+          break;
+        case "position":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              if (!filters.position.value) return true;
+              if (isNaN(filters.position.value)) return true;
+              return shift.position.id == filters.position.value;
+            },
+          };
+          break;
+        case "talent":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              const emp = shift.employees.find(
+                (e) => e.id == filters.talent.value
+              );
+              return emp;
+            },
+          };
+          break;
+        case "venue":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              if (!filters.venue.value) return true;
+              if (isNaN(filters.venue.value)) return true;
+              return shift.venue.id == filters.venue.value;
+            },
+          };
+          break;
+        case "minimum_hourly_rate":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              if (!filters.minimum_hourly_rate.value) return true;
+              if (isNaN(filters.minimum_hourly_rate.value)) return true;
+              return (
+                parseInt(shift.minimum_hourly_rate, 10) >=
+                filters.minimum_hourly_rate.value
+              );
+            },
+          };
+          break;
+        case "date":
+          filters[f] = {
+            value: filters[f],
+            matches: (shift) => {
+              const fdate = moment(filters.date.value);
+              return shift.starting_at.diff(fdate, "days") == 0;
+            },
+          };
+          break;
+        default:
+          throw new Error("Invalid filter");
+      }
+    }
+    return filters;
+  }
+
+  render() {
+    let status = queryString.parse(window.location.search, {
+      arrayFormat: "index",
+    });
+    const groupedShifts = _.groupBy(this.state.shifts, (s) =>
+      moment(s.starting_at).format("MMMM YYYY")
+    );
+    const shiftsHTML = [];
+
+    for (let date in groupedShifts) {
+      shiftsHTML.push(
+        <div key={date} className="date-group">
+          <p className="date-group-label">{date}</p>
+          <div>
+            {groupedShifts[date].map((s, i) => (
+              <ShiftCard key={i} shift={s} showStatus={true} />
+            ))}
+          </div>
+        </div>
+      );
+    }
+    return (
+      <div className="p-1 listcontents">
+        {/* <Wizard continuous
+                steps={this.state.steps}
+                run={this.state.runTutorial}
+                callback={callback}
+                showProgress
+            /> */}
+        <h1 className="float-left">
+          <span id="shift-details-header">Shift Details</span>
+        </h1>
+        {shiftsHTML.length == 0 && (
+          <div className="mt-5">No shifts have been found</div>
+        )}
+        {shiftsHTML}
+        {this.state.showNextButton && shiftsHTML.length != 0 ? (
+          <div className="row text-center w-100 mt-3">
+            <div className="col">
+              <Button
+                onClick={() => {
+                  const PAGINATION_LENGTH = 10;
+                  const NOT_FILLED_SHIFT = `&status=${status.status}`;
+                  const FILLED_SHIFT =
+                    "&filled=true&upcoming=true&not_status=DRAFT&envelope=true";
+                  if (status.status) {
+                    searchMe(
+                      `shifts`,
+                      `?envelope=true&limit=10&offset=${
+                        this.state.offset + PAGINATION_LENGTH
+                      }${
+                        status.status == "FILLED"
+                          ? FILLED_SHIFT
+                          : NOT_FILLED_SHIFT
+                      }`,
+                      this.state.shifts
+                    ).then((newShifts) => {
+                      const showNextButton = newShifts.next !== null;
+                      this.setState({
+                        shifts: newShifts,
+                        offset: this.state.offset + PAGINATION_LENGTH,
+                        showNextButton,
+                      });
+                    });
+                  } else {
+                    searchMe(
+                      `shifts`,
+                      `?envelope=true&limit=10&offset=${
+                        this.state.offset + PAGINATION_LENGTH
+                      }`,
+                      this.state.shifts
+                    ).then((newShifts) => {
+                      const showNextButton = newShifts.next !== null;
+                      this.setState({
+                        shifts: newShifts,
+                        offset: this.state.offset + PAGINATION_LENGTH,
+                        showNextButton,
+                      });
+                    });
+                  }
+                }}
+              >
+                Load More
+              </Button>
+            </div>
+          </div>
+        ) : null}
+      </div>
+    );
+  }
+}
+
+/**
+ * FilterShifts
+ */
+export const FilterShifts = ({ onSave, onCancel, onChange, catalog }) => {
+  const [position, setPosition] = useState("");
+  const [date, setDate] = useState("");
+  const [price, setPrice] = useState("");
+  const [employees, setEmployees] = useState("");
+  const [location, setLocation] = useState("");
+  const [status, setStatus] = useState("");
+  useEffect(() => {
+    const venues = store.getState("venues");
+    if (!venues) fetchAllMe(["venues"]);
+  }, []);
+
+  return (
+    <form>
+      <div className="row">
+        <div className="col">
+          <label>Looking for</label>
+          <Select
+            onChange={(selection) => setPosition(selection.value)}
+            options={catalog.positions}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-8">
+          <label>Date</label>
+          <DateTime
+            timeFormat={false}
+            className="w-100"
+            closeOnSelect={true}
+            renderInput={(properties) => {
+              const { value, ...rest } = properties;
+              return (
+                <input value={value.match(/\d{2}\/\d{2}\/\d{4}/gm)} {...rest} />
+              );
+            }}
+            onChange={(value) => {
+              if (typeof value == "string") {
+                value = moment(value);
+                setDate(value.format("YYYY-MM-DD"));
+              }
+            }}
+          />
+        </div>
+        <div className="col-4">
+          <label>Price / hour</label>
+          <input
+            type="number"
+            className="form-control"
+            onChange={(e) => setPrice(e.target.value)}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col">
+          <label>Location</label>
+          <Select
+            onChange={(selection) => setLocation(selection.value.toString())}
+            options={catalog.venues}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col">
+          <label>Worked by a talent:</label>
+          <SearchCatalogSelect
+            isMulti={true}
+            value={employees}
+            onChange={(selections) => {
+              setEmployees(selections);
+            }}
+            searchFunction={(search) =>
+              new Promise((resolve, reject) =>
+                GET("catalog/employees?full_name=" + search)
+                  .then((talents) =>
+                    resolve(
+                      [
+                        {
+                          label: `${
+                            talents.length == 0 ? "No one found: " : ""
+                          }`,
+                          value: "invite_talent_to_jobcore",
+                        },
+                      ].concat(talents)
+                    )
+                  )
+                  .catch((error) => reject(error))
+              )
+            }
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col">
+          <label>Status</label>
+          <Select
+            onChange={(selection) => setStatus(selection.value.toString())}
+            options={catalog.shiftStatus}
+          />
+        </div>
+      </div>
+      <div className="btn-bar">
+        <button
+          type="button"
+          className="btn btn-primary"
+          onClick={() => {
+            const employeesList =
+              employees != ""
+                ? `&employee=${employees.map((e) => e.value)}`
+                : "";
+            searchMe(
+              `shifts`,
+              `?${
+                status == "FILLED"
+                  ? "filled=true&upcoming=true&not_status=DRAFT"
+                  : "status=" + status
+              }&envelope=true&limit=10&position=${position}&venue=${location}&start=${date}${employeesList}`
+            );
+          }}
+        >
+          Apply Filters
+        </button>
+        <button
+          type="button"
+          className="btn btn-secondary"
+          onClick={() => onSave(false)}
+        >
+          Clear Filters
+        </button>
+      </div>
+    </form>
+  );
+};
+FilterShifts.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  formData: PropTypes.object, //contains the data needed for the form to load
+  onChange: PropTypes.func.isRequired,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+/**
+ * ShiftApplicants
+ */
+export const ShiftApplicants = (props) => {
+  const { onCancel, onSave, catalog } = props;
+  return (
+    <Theme.Consumer>
+      {({ bar }) => (
+        <div className="sidebar-applicants">
+          {catalog.shift.expired ? (
+            <div className="alert alert-warning">
+              This shift has already expired
+            </div>
+          ) : (
+            <div className="top-bar">
+              <button
+                type="button"
+                className="btn btn-primary btn-sm"
+                onClick={() =>
+                  bar.show({
+                    slug: "search_talent_and_invite_to_shift",
+                    data: { shifts: [catalog.shift], employees: [] },
+                    allowLevels: true,
+                  })
+                }
+              >
+                invite
+              </button>
+            </div>
+          )}
+          <h3>Shift applicants:</h3>
+          <ul style={{ overflowY: "auto", maxHeight: "75vh" }}>
+            {catalog.applicants.length > 0 ? (
+              catalog.applicants.map((tal, i) => (
+                <EmployeeExtendedCard
+                  key={i}
+                  employee={tal}
+                  hover={false}
+                  showFavlist={false}
+                  onClick={() =>
+                    bar.show({
+                      slug: "show_single_talent",
+                      data: Talent(tal, i).defaults().unserialize(),
+                      allowLevels: true,
+                    })
+                  }
+                >
+                  {!catalog.shift.expired && (
+                    <Button
+                      className="mt-0"
+                      icon="check"
+                      label="Delete"
+                      onClick={() =>
+                        onSave({
+                          executed_action: "accept_applicant",
+                          applicant: tal,
+                          shift: catalog.shift,
+                        })
+                      }
+                    />
+                  )}
+                  {!catalog.shift.expired && (
+                    <Button
+                      className="mt-0"
+                      icon="times"
+                      label="Delete"
+                      onClick={() =>
+                        onSave({
+                          executed_action: "reject_applicant",
+                          applicant: tal,
+                          shift: catalog.shift,
+                        })
+                      }
+                    />
+                  )}
+                </EmployeeExtendedCard>
+              ))
+            ) : (
+              <li>
+                No applicants were found for this shift,{" "}
+                <span
+                  className="anchor"
+                  onClick={() =>
+                    bar.show({
+                      slug: "search_talent_and_invite_to_shift",
+                      data: { shifts: [catalog.shift], employees: [] },
+                      allowLevels: true,
+                    })
+                  }
+                >
+                  invite more talents
+                </span>{" "}
+                or{" "}
+                <span
+                  className="anchor"
+                  onClick={() =>
+                    bar.show({
+                      slug: "review_shift_invites",
+                      allowLevels: true,
+                      data: catalog.shift,
+                    })
+                  }
+                >
+                  review previous invites
+                </span>
+              </li>
+            )}
+          </ul>
+        </div>
+      )}
+    </Theme.Consumer>
+  );
+};
+ShiftApplicants.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+  context: PropTypes.object, //contact any additional data for context purposes
+};
+
+/**
+ * ShiftApplicants
+ */
+export const ShiftEmployees = (props) => {
+  const { onCancel, onSave, catalog } = props;
+  return (
+    <Theme.Consumer>
+      {({ bar }) => (
+        <div className="sidebar-applicants">
+          {catalog.shift.expired ? (
+            <div className="alert alert-warning">
+              This shift has already expired
+            </div>
+          ) : (
+            <div className="top-bar">
+              <button
+                type="button"
+                className="btn btn-primary btn-sm"
+                onClick={() =>
+                  bar.show({
+                    slug: "search_talent_and_invite_to_shift",
+                    data: { shifts: [catalog.shift] },
+                    allowLevels: true,
+                  })
+                }
+              >
+                invite
+              </button>
+            </div>
+          )}
+          <h3>Scheduled Employees:</h3>
+          {catalog.shift.employees.length > 0 ? (
+            catalog.shift.employees.map((emp, i) => (
+              <EmployeeExtendedCard
+                key={i}
+                employee={emp}
+                hover={false}
+                showFavlist={false}
+                onClick={() =>
+                  bar.show({
+                    slug: "show_single_talent",
+                    data: Talent(emp).defaults().unserialize(),
+                    allowLevels: true,
+                  })
+                }
+              >
+                <Button
+                  className="mt-0 text-black"
+                  icon="clock"
+                  label="Clockin log"
+                  onClick={() =>
+                    bar.show({
+                      slug: "talent_shift_clockins",
+                      data: { employee: emp, shift: catalog.shift },
+                      allowLevels: true,
+                    })
+                  }
+                />
+
+                {!catalog.shift.expired && (
+                  <Button
+                    className="mt-0 text-black"
+                    icon="trash"
+                    label="Delete"
+                    onClick={() => {
+                      const noti = Notify.info(
+                        "Are you sure? The Talent will be kicked out of this shift",
+                        (answer) => {
+                          if (catalog.showShift) {
+                            if (answer) {
+                              deleteShiftEmployee(catalog.shift.id, emp);
+                              catalog.shift.employees =
+                                catalog.shift.employees.filter(
+                                  (e) => e.id == emp.id
+                                );
+                            }
+                          } else {
+                            if (answer) {
+                              onSave({
+                                executed_action: "delete_shift_employee",
+                                employee: emp,
+                                shift: catalog.shift,
+                              });
+                            }
+                          }
+                          noti.remove();
+                        }
+                      );
+                    }}
+                  />
+                )}
+              </EmployeeExtendedCard>
+            ))
+          ) : catalog.shift.expired ? (
+            <p>No talents every worked on this shift</p>
+          ) : (
+            <p>
+              No talents have been accepted for this shift yet,{" "}
+              <span
+                className="anchor"
+                onClick={() =>
+                  bar.show({
+                    slug: "search_talent_and_invite_to_shift",
+                    data: { shifts: [catalog.shift] },
+                    allowLevels: true,
+                  })
+                }
+              >
+                invite more talents
+              </span>{" "}
+              or{" "}
+              <span
+                className="anchor"
+                onClick={() =>
+                  bar.show({
+                    slug: "review_shift_invites",
+                    allowLevels: true,
+                    data: catalog.shift,
+                  })
+                }
+              >
+                review previous invites
+              </span>
+            </p>
+          )}
+        </div>
+      )}
+    </Theme.Consumer>
+  );
+};
+ShiftEmployees.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+  context: PropTypes.object, //contact any additional data for context purposes
+};
+
+/**
+ * ShiftApplicants
+ */
+export const ShiftInvites = ({ onCancel, onSave, formData }) => {
+  const { bar } = useContext(Theme.Context);
+  const status = {
+    PENDING: "Waiting for reponse",
+    APPLIED: "The talent applied",
+    REJECTED: "The talent reject it",
+  };
+  //formData.shift.maximum_allowed_employees
+  //formData.shift.status != "OPEN"
+  const htmlInvites = formData.invites.map((invite, i) => (
+    <GenericCard
+      key={i}
+      className="pr-2"
+      onClick={() =>
+        bar.show({
+          slug: "show_single_talent",
+          data: invite.employee,
+          allowLevels: true,
+        })
+      }
+    >
+      <Avatar
+        url={
+          invite.employee.user.profile
+            ? invite.employee.user.profile.picture
+            : "https://res.cloudinary.com/hq02xjols/image/upload/v1560365062/static/default_profile1.png"
+        }
+      />
+      <p>
+        <b>
+          {invite.employee.user.first_name +
+            " " +
+            invite.employee.user.last_name}
+        </b>
+      </p>
+      <p className="mr-2 my-0">
+        Sent {moment(invite.created_at).fromNow()} and{" "}
+        <span className="badge">{status[invite.status]}</span>
+      </p>
+    </GenericCard>
+  ));
+  return (
+    <div className="sidebar-applicants">
+      <h3>Already invited to this shift:</h3>
+      {formData.shift.status != "OPEN" && (
+        <div className="alert alert-warning">
+          This shift is not accepting any more candidates, this invites will be
+          erased soon.
+        </div>
+      )}
+      {formData.shift.maximum_allowed_employees <=
+        formData.shift.employees.length && (
+        <div className="alert alert-warning">
+          This shift is full (filled), this invites will be erased soon.
+        </div>
+      )}
+      {htmlInvites.length > 0 ? htmlInvites : <p>No invites have been sent</p>}
+    </div>
+  );
+};
+ShiftInvites.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  formData: PropTypes.object, //contains the data needed for the form to load
+  context: PropTypes.object, //contact any additional data for context purposes
+};
+
+/**
+ * EditOrAddShift
+ */
+const EditOrAddShift = ({
+  onSave,
+  onCancel,
+  onChange,
+  catalog,
+  formData,
+  error,
+  bar,
+  oldShift,
+}) => {
+  const [expired, setExpired] = useState()
+  const [runTutorial, setRunTutorial] = useState(hasTutorial());
+  const [steps, setSteps] = useState([
+    {
+      target: "#looking-for",
+      content: "Here you can select what position you are looking for",
+      placement: "left",
+      disableOverlay: true,
+      disableBeacon: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#how-many",
+      content: "Edit the total numbers of talents needed for this shift",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#price",
+      content: "Edit how much you are willing to pay per hour",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#date-shift",
+      content: "Enter the data. Click more to add additional dates",
+      placement: "left",
+      allowClicksThruHole: true,
+      spotlightClicks: true,
+      disableOverlay: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#from-to-date",
+      content: "Edit the starting time and ending time of the shift",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#location",
+      content:
+        "Select the location for which the shift it's taking. You can add a new location by selecting add new location option",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#instruction",
+      content:
+        "Add instruction or any information about the shift if necessary that could help the employees",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#who-can-apply",
+      content:
+        "Select who can apply for this shift or broadcast this shift for all qualified employees in JobCore.",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+    {
+      target: "#publish",
+      content: "Publish your shift notify invited talents",
+      placement: "left",
+      allowClicksThruHole: true,
+      disableOverlay: true,
+      spotlightClicks: true,
+      styles: {
+        options: {
+          zIndex: 10000,
+        },
+      },
+      locale: { skip: "Skip Tutorial" },
+    },
+  ]);
+  const [description, setDescription] = useState("");
+  const [tutorial, setTutorial] = useState(false);
+  const [recurrent, setRecurrent] = useState();
+  const [recurrentDates, setRecurrentDates] = useState({
+    starting_at: moment(),
+    ending_at: moment().add(1, "M"),
+  });
+  const [totalShift, setTotalShift] = useState(0);
+
+  const [recurrentTimes, setRecurrentTimes] = useState({
+    sunday: {
+      active: false,
+      starting_at: null,
+      ending_at: null,
+    },
+    monday: {
+      active: true,
+      starting_at: null,
+      ending_at: null,
+    },
+    tuesday: {
+      active: true,
+      starting_at: null,
+      ending_at: null,
+    },
+    wednesday: {
+      active: true,
+      starting_at: null,
+      ending_at: null,
+    },
+    thursday: {
+      active: true,
+      starting_at: null,
+      ending_at: null,
+    },
+    friday: {
+      active: true,
+      starting_at: null,
+      ending_at: null,
+    },
+    saturday: {
+      active: false,
+      starting_at: null,
+      ending_at: null,
+    },
+  });
+  const [multipleRecurrentShift, setMultipleRecurrentShift] = useState([]);
+  const [previousShifts, setPreviousShifts] = useState([]);
+  const [totalHoursEmployeeWeek, settotalHoursEmployeeWeek] = useState(null);
+
+  const setDescriptionContent = (description) => {
+    description.length > 300
+      ? setDescription(description.slice(0, 300))
+      : setDescription(description);
+  };
+
+  const getRecurrentShifts = async function getRecurrentDates() {
+    var startDate = recurrentDates.starting_at;
+    var endDate = recurrentDates.ending_at;
+
+    const start = startDate.startOf("days");
+    const end = endDate.startOf("days");
+
+    let weekDays = Object.values([recurrentTimes][0]);
+
+    const dailyInfo = weekDays;
+    let totalDays = 0;
+    var multipleShifts = [];
+
+    dailyInfo.forEach((info, index) => {
+      if (info.active === true && info.starting_at && info.ending_at) {
+        let current = start.clone();
+        if (current.isoWeekday() <= index) {
+          current = current.isoWeekday(index);
+        } else {
+          current.add(1, "weeks").isoWeekday(index);
+        }
+
+        while (current.isSameOrBefore(end)) {
+          const starting = moment(
+            current.format("MM-DD-YYYY") +
+              " " +
+              info.starting_at.format("hh:mm a"),
+            "MM-DD-YYYY hh:mm a"
+          );
+          const ending = moment(
+            current.format("MM-DD-YYYY") +
+              " " +
+              info.ending_at.format("hh:mm a"),
+            "MM-DD-YYYY hh:mm a"
+          );
+
+          multipleShifts.push({ starting_at: starting, ending_at: ending });
+          current.day(7 + index);
+          totalDays += 1;
+        }
+      }
+    });
+
+    setTotalShift(totalDays);
+    setMultipleRecurrentShift(multipleShifts);
+    return multipleShifts;
+  };
+  const saveRecurrentDates = async function saveRecurrentDates() {
+    await getRecurrentShifts().then((res) => {
+      if (res && Array.isArray(res) && res.length > 0) {
+        const noti = Notify.info(
+          `Are you sure to publish a total of ${res.length}? (
+                    ${
+                      "From " +
+                      res[0].starting_at.format("MM-DD-YYYY") +
+                      " - " +
+                      res[res.length - 1].ending_at.format("MM-DD-YYYY") +
+                      " "
+                      // res.map(s => {
+                      //     return s.starting_at.format("MM-DD-YYYY hh:mm a") + " - " + s.ending_at.format("MM-DD-YYYY hh:mm a") + " ";
+                      // })
+                    }
+                )`,
+          (answer) => {
+            if (answer) {
+              formData.multiple_dates = res;
+              onSave({
+                executed_action: isNaN(formData.id)
+                  ? "create_shift"
+                  : "update_shift",
+                status: "OPEN",
+              });
+              // window.location.reload();
+            }
+            noti.remove();
+          }
+        );
+        return noti;
+      } else alert("Error: Please select recurrent dates.  ");
+    });
+  };
+  async function getPreviousShift(emp) {
+    let response = await GET(
+      "employers/me/shifts?employee=" + emp + "&limit=15"
+    ).then((res) => {
+      const previous_shifts = res.filter(
+        (v, i, a) =>
+          a.findIndex((t) => t.position.id === v.position.id) === i &&
+          v.position.id == parseInt(formData.position)
+      );
+          
+      var start_payroll = moment().clone().weekday(1);
+      var end_payroll = moment(start_payroll).add(6, "days");
+      const payrollWeekShifts =
+        res.filter((e) =>
+          moment(e.starting_at).isBetween(
+            start_payroll,
+            end_payroll,
+            "day",
+            "[]"
+          )
+        ) || [];
+
+      const scheduleHours = payrollWeekShifts.reduce(
+        (total, { starting_at, ending_at }) =>
+          total +
+          moment
+            .duration(moment(ending_at).diff(moment(starting_at)))
+            .asHours(),
+        0
+      );
+      console.log("scheduleHours###", scheduleHours)
+      setPreviousShifts(previous_shifts);
+      settotalHoursEmployeeWeek(scheduleHours);
+    });
+
+    return response;
+  }
+
+  useEffect(() => {
+    setRecurrent(true)
+    const venues = store.getState("venues");
+    const favlists = store.getState("favlists");
+    if (!venues || !favlists) fetchAllMe(["venues", "favlists"]);
+  }, []);
+  useEffect(() => {
+    setExpired(moment(formData.starting_at).isBefore(NOW()) || moment(formData.ending_at).isBefore(NOW()))
+  }, [formData.starting_at]);
+  
+  if (
+    catalog.positions.find(
+      (pos) =>
+        pos.value == formData.position.id || pos.value == formData.position
+    )
+  )
+    formData["position"] = catalog.positions
+      .find(
+        (pos) =>
+          pos.value == formData.position.id || pos.value == formData.position
+      )
+      .value.toString();
+  if (
+    catalog.venues.find(
+      (pos) => pos.value == formData.venue.id || pos.value == formData.venue
+    )
+  )
+    formData["venue"] = catalog.venues
+      .find(
+        (pos) => pos.value == formData.venue.id || pos.value == formData.venue
+      )
+      .value.toString();
+  if (formData.employer && isNaN(formData.employer))
+    formData.employer = formData.employer.id;
+
+  if (!formData.shift && !isNaN(formData.id)) formData.shift = formData.id;
+  if (formData.required_badges) delete formData.required_badges;
+  if (description) formData.description = description;
+  
+  const handleChange = e => {if (e.target.value==="true") {
+    setRecurrent(false) 
+  } else if (e.target.value==="false") {
+    setRecurrent(true)
+  }}
+    
+  return (
+    <div>
+      {/* <Wizard continuous 
+            steps={steps}
+            run={tutorial}
+            callback={callback}
+            disableBeacon={true}
+            styles={{
+                options: {
+                primaryColor: '#000',
+                }
+            }}
+            /> */}
+
+      <div
+        style={{
+          overflowY: "auto",
+          overflowX: "hidden",
+          height: "calc(100vh - 75px)",
+        }}
+      >
+        <form>
+          {/* <div className="row">
+                        <div className="col-12 text-right">
+                            <button type="button" className="btn btn-primary p-1 text-right" onClick={()=>setTutorial(true)}><strong>HELP ?</strong></button>
+                        </div>
+                    </div> */}
+          <div className="row">
+            <div className="col-12">
+              {formData.hide_warnings === true ? null : formData.status ==
+                  "DRAFT" && !error ? (
+                <div className="alert alert-warning d-inline">
+                  <i className="fas fa-exclamation-triangle"></i> This shift is
+                  a draft
+                </div>
+              ) : formData.status != "UNDEFINED" && !error ? (
+                <div className="alert alert-success">
+                  This shift is published, therefore{" "}
+                  <strong>it needs to be unpublished</strong> before it can be
+                  updated
+                </div>
+              ) : (
+                ""
+              )}
+            </div>
+          </div>
+
+          <div className="row" id="looking-for">
+            <div className="col-12">
+              <label>Looking for</label>
+
+              <Select
+                placeholder="Select a position"
+                value={catalog.positions.find(
+                  (pos) =>
+                    pos.value == formData.position.id ||
+                    pos.value == formData.position
+                )}
+                onChange={(selection) => {
+                  onChange({
+                    position: selection.value.toString(),
+                    has_sensitive_updates: true,
+                  });
+                  if (
+                    Array.isArray(formData.pending_invites) &&
+                    formData.pending_invites.length == 1 &&
+                    formData.position
+                  ) {
+                    getPreviousShift(formData.pending_invites[0].value);
+                  } else setPreviousShifts([]);
+                }}
+                options={catalog.positions}
+              />
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-6" id="how-many">
+              <label>How many?</label>
+              <input
+                type="number"
+                className="form-control"
+                value={formData.maximum_allowed_employees}
+                onChange={(e) => {
+                  if (parseInt(e.target.value, 10) > 0) {
+                    if (
+                      oldShift &&
+                      oldShift.employees.length > parseInt(e.target.value, 10)
+                    )
+                      Notify.error(
+                        `${oldShift.employees.length} talents are scheduled to work on this shift already, delete scheduled employees first.`
+                      );
+                    else
+                      onChange({ maximum_allowed_employees: e.target.value });
+                  }
+                }}
+              />
+            </div>
+            <div className="col-6" id="price">
+              <label>Price / hour</label>
+              <input
+                type="number"
+                className="form-control"
+                value={formData.minimum_hourly_rate}
+                onChange={(e) =>
+                  onChange({
+                    minimum_hourly_rate: e.target.value,
+                    has_sensitive_updates: true,
+                  })
+                }
+              />
+              {Array.isArray(previousShifts) && previousShifts.length == 1 ? (
+                <span
+                  className="badge badge-primary"
+                  style={{ cursor: "pointer" }}
+                  onClick={() =>
+                    onChange({
+                      minimum_hourly_rate: parseFloat(
+                        previousShifts[0]["minimum_hourly_rate"]
+                      ),
+                    })
+                  }
+                >
+                  Previous $/hr: $
+                  {parseFloat(previousShifts[0]["minimum_hourly_rate"]).toFixed(
+                    2
+                  )}
+                </span>
+              ) : null}
+            </div>
+          </div>
+          <div className="row mt-3 mb-1" id="date-shift">
+            <div className="col-12">
+              <label className="mb-1">Create recurrent shifts?</label>
+              <div className="form-check form-check-inline ml-2">
+                <input
+                  className="form-check-input"
+                  type="radio"
+                  name="recurrentShifts"
+                  id="recurrentYes"
+                  value={true}
+                  style={{ verticalAlign: "middle" }}
+                  onChange={handleChange}
+                />
+                <span className="form-check-label" htmlFor="recurrentYes">
+                  Yes
+                </span>
+              </div>
+              <div className="form-check form-check-inline">
+                <input
+                  className="form-check-input"
+                  type="radio"
+                  name="recurrentShifts"
+                  id="recurrentNo"
+                  value={false}
+                  defaultChecked
+                  onChange={handleChange}
+                />
+                <span className="form-check-label" htmlFor="recurrentNo">
+                  No
+                </span>
+              </div>
+            </div>
+            {!recurrent && (
+              <div className="col-12 mt-2">
+                <div className="row text-center">
+                  <div className="col" />
+                  <div className="col">
+                    <span>From</span>
+                  </div>
+                  <div className="col">
+                    <span>To</span>
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.monday.active}
+                        checked={recurrentTimes.monday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            monday: {
+                              active: !recurrentTimes.monday.active,
+                              starting_at: recurrentTimes.monday.starting_at,
+                              ending_at: recurrentTimes.monday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Monday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.monday.starting_at}
+                      inputProps={{
+                        disabled: !recurrentTimes.monday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          monday: {
+                            active: recurrentTimes.monday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.monday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.monday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.monday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          monday: {
+                            active: recurrentTimes.monday.active,
+                            starting_at: recurrentTimes.monday.starting_at,
+                            ending_at: value,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.tuesday.active}
+                        checked={recurrentTimes.tuesday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            tuesday: {
+                              active: !recurrentTimes.tuesday.active,
+                              starting_at: recurrentTimes.tuesday.starting_at,
+                              ending_at: recurrentTimes.tuesday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Tuesday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.tuesday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.tuesday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          tuesday: {
+                            active: recurrentTimes.tuesday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.tuesday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.tuesday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.tuesday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          tuesday: {
+                            active: recurrentTimes.tuesday.active,
+                            starting_at: recurrentTimes.tuesday.starting_at,
+                            ending_at: value,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.wednesday.active}
+                        checked={recurrentTimes.wednesday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            wednesday: {
+                              active: !recurrentTimes.wednesday.active,
+                              starting_at: recurrentTimes.wednesday.starting_at,
+                              ending_at: recurrentTimes.wednesday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Wednesday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.wednesday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.wednesday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          wednesday: {
+                            active: recurrentTimes.wednesday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.wednesday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.wednesday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.wednesday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          wednesday: {
+                            active: recurrentTimes.wednesday.active,
+                            starting_at: recurrentTimes.wednesday.starting_at,
+                            ending_at: value,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.thursday.active}
+                        checked={recurrentTimes.thursday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            thursday: {
+                              active: !recurrentTimes.thursday.active,
+                              starting_at: recurrentTimes.thursday.starting_at,
+                              ending_at: recurrentTimes.thursday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Thursday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.thursday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.thursday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          thursday: {
+                            active: recurrentTimes.thursday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.thursday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      inputProps={{
+                        disabled: !recurrentTimes.thursday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeFormat={DATETIME_FORMAT}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.thursday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          thursday: {
+                            active: recurrentTimes.thursday.active,
+                            ending_at: value,
+                            starting_at: recurrentTimes.thursday.starting_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.friday.active}
+                        checked={recurrentTimes.friday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            friday: {
+                              active: !recurrentTimes.friday.active,
+                              starting_at: recurrentTimes.friday.starting_at,
+                              ending_at: recurrentTimes.friday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Friday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.friday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.friday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          friday: {
+                            active: recurrentTimes.friday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.friday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.friday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.friday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          friday: {
+                            active: recurrentTimes.friday.active,
+                            ending_at: value,
+                            starting_at: recurrentTimes.friday.starting_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.saturday.active}
+                        checked={recurrentTimes.saturday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            saturday: {
+                              active: !recurrentTimes.saturday.active,
+                              starting_at: recurrentTimes.saturday.starting_at,
+                              ending_at: recurrentTimes.saturday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Saturday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.saturday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.saturday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          saturday: {
+                            active: recurrentTimes.saturday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.saturday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.saturday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.saturday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          saturday: {
+                            active: recurrentTimes.saturday.active,
+                            ending_at: value,
+                            starting_at: recurrentTimes.saturday.starting_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row mb-1" id="from-to-date">
+                  <div className="col my-auto">
+                    <div className="form-check">
+                      <input
+                        className="form-check-input"
+                        type="checkbox"
+                        value={recurrentTimes.sunday.active}
+                        checked={recurrentTimes.sunday.active}
+                        onChange={() =>
+                          setRecurrentTimes({
+                            ...recurrentTimes,
+                            sunday: {
+                              active: !recurrentTimes.sunday.active,
+                              starting_at: recurrentTimes.sunday.starting_at,
+                              ending_at: recurrentTimes.sunday.ending_at,
+                            },
+                          })
+                        }
+                        id="defaultCheck1"
+                      />
+                      <span
+                        className="form-check-label"
+                        htmlFor="defaultCheck1"
+                      >
+                        Sunday
+                      </span>
+                    </div>
+                  </div>
+                  <div className="col">
+                    <DateTime
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.sunday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      closeOnTab={true}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.sunday.starting_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          sunday: {
+                            active: recurrentTimes.sunday.active,
+                            starting_at: value,
+                            ending_at: recurrentTimes.sunday.ending_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                  <div className="col-4">
+                    {/* <label>To {(formData.ending_at.isBefore(formData.starting_at)) && "(next day)"}</label> */}
+                    <DateTime
+                      className="picker-left"
+                      dateFormat={false}
+                      timeFormat={DATETIME_FORMAT}
+                      inputProps={{
+                        disabled: !recurrentTimes.sunday.active,
+                        placeholder: "0:00 am",
+                      }}
+                      timeConstraints={{ minutes: { step: 15 } }}
+                      value={recurrentTimes.sunday.ending_at}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        if (typeof value == "string") value = moment(value);
+                        setRecurrentTimes({
+                          ...recurrentTimes,
+                          sunday: {
+                            active: recurrentTimes.sunday.active,
+                            ending_at: value,
+                            starting_at: recurrentTimes.sunday.starting_at,
+                          },
+                        });
+                      }}
+                    />
+                  </div>
+                </div>
+                <div className="row" id="date-shift">
+                  <div className="col-6">
+                    <label className="mb-1">Starting Date</label>
+
+                    <div className="input-group">
+                      <DateTime
+                        timeFormat={false}
+                        className="shiftdate-picker"
+                        closeOnSelect={true}
+                        value={recurrentDates.starting_at}
+                        isValidDate={(current) => {
+                          return current.isAfter(YESTERDAY) ? true : false;
+                        }}
+                        renderInput={(properties) => {
+                          const { value, ...rest } = properties;
+                          return (
+                            <input
+                              value={value.match(/\d{2}\/\d{2}\/\d{4}/gm)}
+                              {...rest}
+                            />
+                          );
+                        }}
+                        onChange={(value) => {
+                          if (typeof value == "string") value = moment(value);
+                          const getRealDate = (start, end) => {
+                            const starting = moment(
+                              value.format("MM-DD-YYYY"),
+                              "MM-DD-YYYY"
+                            );
+                            var ending = moment(end);
+                            if (
+                              typeof starting !== "undefined" &&
+                              starting.isValid()
+                            ) {
+                              if (ending.isBefore(starting)) {
+                                ending = ending.add(1, "days");
+                              }
+
+                              return setRecurrentDates({
+                                starting_at: starting,
+                                ending_at: ending,
+                              });
+                            }
+                            return null;
+                          };
+
+                          getRealDate(
+                            recurrentDates.starting_at,
+                            recurrentDates.ending_at
+                          );
+                        }}
+                      />
+                    </div>
+                  </div>
+                  {/* <div className="col-12"/> */}
+                  <div className="col-6">
+                    <label className="mb-1">Ending Date</label>
+                    <div className="input-group">
+                      <DateTime
+                        timeFormat={false}
+                        className="picker-left"
+                        closeOnSelect={true}
+                        value={recurrentDates.ending_at}
+                        isValidDate={(current) => {
+                          return current.isAfter(YESTERDAY) &&
+                            current.isBefore(moment().add(36, "M"))
+                            ? true
+                            : false;
+                        }}
+                        renderInput={(properties) => {
+                          const { value, ...rest } = properties;
+                          return (
+                            <input
+                              value={value.match(/\d{2}\/\d{2}\/\d{4}/gm)}
+                              {...rest}
+                            />
+                          );
+                        }}
+                        onChange={(value) => {
+                          if (typeof value == "string") value = moment(value);
+
+                          const getRealDate = (start, end) => {
+                            const starting = start;
+                            var ending = moment(
+                              value.format("MM-DD-YYYY"),
+                              "MM-DD-YYYY"
+                            );
+
+                            if (
+                              typeof starting !== "undefined" &&
+                              starting.isValid()
+                            ) {
+                              if (ending.isBefore(starting)) {
+                                ending = ending.add(1, "days");
+                              }
+
+                              return setRecurrentDates({
+                                starting_at: starting,
+                                ending_at: ending,
+                              });
+                            }
+                            return null;
+                          };
+
+                          getRealDate(
+                            recurrentDates.starting_at,
+                            recurrentDates.ending_at
+                          );
+                        }}
+                      />
+                    </div>
+                  </div>
+                </div>
+              </div>
+            )}
+          </div>
+          {recurrent && (
+            <div>
+              <div className="row" id="date-shift">
+                <div className="col-12">
+                  <label className="mb-1">Dates</label>
+                  {formData.multiple_dates && (
+                    <p className="mb-1 mt-0">
+                      {formData.multiple_dates.map((d, i) => (
+                        <span key={i} className="badge">
+                          {d.starting_at.format("MM-DD-YYYY")}
+                          <i
+                            className="fas fa-trash-alt ml-1 pointer"
+                            onClick={() =>
+                              onChange({
+                                multiple_dates: !formData.multiple_dates
+                                  ? []
+                                  : formData.multiple_dates.filter(
+                                      (dt) =>
+                                        !dt.starting_at.isSame(d.starting_at)
+                                    ),
+                                has_sensitive_updates: true,
+                              })
+                            }
+                          />
+                        </span>
+                      ))}
+                    </p>
+                  )}
+                  <div className="input-group">
+                    <DateTime
+                      timeFormat={false}
+                      className="shiftdate-picker"
+                      closeOnSelect={true}
+                      value={formData.starting_at}
+                      isValidDate={(current) => {
+                        return formData.multiple_dates !== undefined &&
+                          formData.multiple_dates.length > 0
+                          ? current.isAfter(YESTERDAY)
+                          : true;
+                      }}
+                      renderInput={(properties) => {
+                        const { value, ...rest } = properties;
+                        return (
+                          <input
+                            value={value.match(/\d{2}\/\d{2}\/\d{4}/gm)}
+                            {...rest}
+                          />
+                        );
+                      }}
+                      onChange={(value) => {
+                        const getRealDate = (start, end) => {
+                          if (typeof start == "string") value = moment(start);
+
+                          const starting = moment(
+                            start.format("MM-DD-YYYY") +
+                              " " +
+                              start.format("hh:mm a"),
+                            "MM-DD-YYYY hh:mm a"
+                          );
+
+                          var ending = moment(
+                            start.format("MM-DD-YYYY") +
+                              " " +
+                              end.format("hh:mm a"),
+                            "MM-DD-YYYY hh:mm a"
+                          );
+                          if (
+                            typeof starting !== "undefined" &&
+                            starting.isValid()
+                          ) {
+                            if (ending.isBefore(starting)) {
+                              ending = ending.add(1, "days");
+                            }
+
+                            return { starting_at: starting, ending_at: ending };
+                          }
+                          return null;
+                        };
+
+                        const mainDate = getRealDate(value, formData.ending_at);
+
+                        const multipleDates = !Array.isArray(
+                          formData.multiple_dates
+                        )
+                          ? []
+                          : formData.multiple_dates.map((d) =>
+                              getRealDate(d.starting_at, d.ending_at)
+                            );
+                        onChange({
+                          ...mainDate,
+                          multiple_dates: multipleDates,
+                          has_sensitive_updates: true,
+                        });
+                      }}
+                    />
+                    <div
+                      className="input-group-append"
+                      onClick={() => {
+                        if (expired)
+                          Notify.error(
+                            "Shifts with and expired starting or ending times cannot have multiple dates or be recurrent"
+                          );
+                        else
+                          onChange({
+                            multiple_dates: !formData.multiple_dates
+                              ? [
+                                  {
+                                    starting_at: formData.starting_at,
+                                    ending_at: formData.ending_at,
+                                  },
+                                ]
+                              : formData.multiple_dates
+                                  .filter(
+                                    (dt) =>
+                                      !dt.starting_at.isSame(
+                                        formData.starting_at
+                                      )
+                                  )
+                                  .concat({
+                                    starting_at: formData.starting_at,
+                                    ending_at: formData.ending_at,
+                                  }),
+                            has_sensitive_updates: true,
+                          });
+                      }}
+                    >
+                      <span className="input-group-text pointer">
+                        More <i className="fas fa-plus ml-1"></i>
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              <div className="row" id="from-to-date">
+                <div className="col-6">
+                  <label>From</label>
+                  <DateTime
+                    dateFormat={false}
+                    timeFormat={DATETIME_FORMAT}
+                    closeOnTab={true}
+                    timeConstraints={{ minutes: { step: 15 } }}
+                    value={formData.starting_at}
+                    renderInput={(properties) => {
+                      const { value, ...rest } = properties;
+                      return (
+                        <input
+                          value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                          {...rest}
+                        />
+                      );
+                    }}
+                    onChange={(value) => {
+                      if (typeof value == "string") value = moment(value);
+
+                      const getRealDate = (start, end) => {
+                        const starting = moment(
+                          start.format("MM-DD-YYYY") +
+                            " " +
+                            value.format("hh:mm a"),
+                          "MM-DD-YYYY hh:mm a"
+                        );
+
+                        var ending = moment(end);
+                        if (
+                          typeof starting !== "undefined" &&
+                          starting.isValid()
+                        ) {
+                          if (ending.isBefore(starting)) {
+                            ending = ending.add(1, "days");
+                          }
+
+                          return { starting_at: starting, ending_at: ending };
+                        }
+                        return null;
+                      };
+
+                      const mainDate = getRealDate(
+                        formData.starting_at,
+                        formData.ending_at
+                      );
+                      const multipleDates = !Array.isArray(
+                        formData.multiple_dates
+                      )
+                        ? []
+                        : formData.multiple_dates.map((d) =>
+                            getRealDate(d.starting_at, d.ending_at)
+                          );
+                      onChange({
+                        ...mainDate,
+                        multiple_dates: multipleDates,
+                        has_sensitive_updates: true,
+                      });
+                    }}
+                  />
+                </div>
+                <div className="col-6">
+                  <label>
+                    To{" "}
+                    {formData.ending_at.isBefore(formData.starting_at) &&
+                      "(next day)"}
+                  </label>
+                  <DateTime
+                    className="picker-left"
+                    dateFormat={false}
+                    timeFormat={DATETIME_FORMAT}
+                    timeConstraints={{ minutes: { step: 15 } }}
+                    value={formData.ending_at}
+                    renderInput={(properties) => {
+                      const { value, ...rest } = properties;
+                      return (
+                        <input
+                          value={value.match(/\d{1,2}:\d{1,2}\s?[ap]m/gm)}
+                          {...rest}
+                        />
+                      );
+                    }}
+                    onChange={(value) => {
+                      if (typeof value == "string") value = moment(value);
+
+                      const getRealDate = (start, end) => {
+                        const starting = start;
+                        var ending = moment(
+                          start.format("MM-DD-YYYY") +
+                            " " +
+                            value.format("hh:mm a"),
+                          "MM-DD-YYYY hh:mm a"
+                        );
+
+                        if (
+                          typeof starting !== "undefined" &&
+                          starting.isValid()
+                        ) {
+                          if (ending.isBefore(starting)) {
+                            ending = ending.add(1, "days");
+                          }
+
+                          return { starting_at: starting, ending_at: ending };
+                        }
+                        return null;
+                      };
+
+                      const mainDate = getRealDate(
+                        formData.starting_at,
+                        formData.ending_at
+                      );
+                      const multipleDates = !Array.isArray(
+                        formData.multiple_dates
+                      )
+                        ? []
+                        : formData.multiple_dates.map((d) =>
+                            getRealDate(d.starting_at, d.ending_at)
+                          );
+                      onChange({
+                        ...mainDate,
+                        multiple_dates: multipleDates,
+                        has_sensitive_updates: true,
+                      });
+                    }}
+                  />
+                </div>
+              </div>
+            </div>
+          )}
+          <div className="row" id="location">
+            <div className="col-12">
+              <label>Location</label>
+              <Select
+                value={catalog.venues.find(
+                  (ven) =>
+                    ven.value == formData.venue.id ||
+                    ven.value == formData.venue
+                )}
+                options={[
+                  {
+                    label: "Add a location",
+                    value: "new_venue",
+                    component: AddOrEditLocation,
+                  },
+                ].concat(catalog.venues)}
+                onChange={(selection) => {
+                  if (selection.value == "new_venue")
+                    bar.show({ slug: "create_location", allowLevels: true });
+                  else
+                    onChange({
+                      venue: selection.value.toString(),
+                      has_sensitive_updates: true,
+                    });
+                }}
+              />
+            </div>
+          </div>
+          <div className="row" id="instruction">
+            <div className="col-12">
+              <label>Shift Instructions (optional)</label>
+              <TextareaAutosize
+                minRows={2}
+                style={{ width: "100%" }}
+                placeholder="Dressing code, location instructions, parking instruction etc.."
+                onChange={(event) => setDescriptionContent(event.target.value)}
+                value={description}
+              />
+            </div>
+          </div>
+
+          <div className="row mt-3">
+            <div className="col-12" id="who-can-apply">
+              <h4>Who can apply to this shift?</h4>
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12">
+              {expired ? (
+                <div className="alert alert-warning">
+                  This shift has an expired date, therefore you cannot invite
+                  anyone but you can still use it for payroll purposes.
+                </div>
+              ) : (
+                <Select
+                  value={catalog.applicationRestrictions.find(
+                    (a) => a.value == formData.application_restriction
+                  )}
+                  onChange={(selection) =>
+                    onChange({
+                      application_restriction: selection.value.toString(),
+                    })
+                  }
+                  options={catalog.applicationRestrictions}
+                />
+              )}
+            </div>
+          </div>
+          {!expired && formData.application_restriction == "FAVORITES" ? (
+            <div className="row">
+              <div className="col-12">
+                <label>From these favorite lists</label>
+                <Select
+                  isMulti
+                  value={formData.allowedFavlists}
+                  onChange={(opt) => onChange({ allowedFavlists: opt })}
+                  options={catalog.favlists}
+                ></Select>
+              </div>
+            </div>
+          ) : !expired && formData.application_restriction == "ANYONE" ? (
+            <div className="row mt-3">
+              <div className="col-5">
+                <label className="mt-2">Minimum rating</label>
+              </div>
+              <div className="col-7">
+                <Select
+                  value={catalog.stars.find(
+                    (s) => s.value == formData.minimum_allowed_rating
+                  )}
+                  onChange={(selection) =>
+                    onChange({ minimum_allowed_rating: selection.value })
+                  }
+                  options={catalog.stars}
+                />
+              </div>
+            </div>
+          ) : (
+            !expired && (
+              <div className="row">
+                <div className="col-12">
+                  <label>Search people in JobCore:</label>
+                  <SearchCatalogSelect
+                    isMulti={true}
+                    value={formData.pending_invites}
+                    onChange={(selections) => {
+                      const invite = selections.find(
+                        (opt) => opt.value == "invite_talent_to_jobcore"
+                      );
+                      if (invite)
+                        bar.show({
+                          allowLevels: true,
+                          slug: "invite_talent_to_jobcore",
+                          onSave: (emp) =>
+                            onChange({
+                              pending_jobcore_invites:
+                                formData.pending_jobcore_invites.concat(emp),
+                            }),
+                        });
+                      else onChange({ pending_invites: selections });
+                      if (
+                        Array.isArray(formData.pending_invites) &&
+                        formData.pending_invites.length == 1 &&
+                        formData.position
+                      ) {
+                        getPreviousShift(formData.pending_invites[0].value);
+                      } else {
+                        setPreviousShifts([]);
+                        settotalHoursEmployeeWeek(null);
+                      }
+                    }}
+                    searchFunction={(search) =>
+                      new Promise((resolve, reject) =>
+                        GET("catalog/employees?full_name=" + search)
+                          .then((talents) => {
+                            resolve(
+                              [
+                                {
+                                  label: `${
+                                    talents.length == 0 ? "No one found: " : ""
+                                  }Invite "${search}" to jobcore`,
+                                  value: "invite_talent_to_jobcore",
+                                },
+                              ].concat(talents)
+                            )
+                            })
+                          .catch((error) => reject(error))
+                      )
+                    }
+                  />
+                </div>
+              </div>
+            )
+          )}
+          {formData.pending_jobcore_invites.length > 0 ? (
+            <div className="row">
+              <div className="col-12">
+                <p className="m-0 p-0">
+                  The following people will be invited to this shift after they
+                  accept your invitation to jobcore:
+                </p>
+                {formData.pending_jobcore_invites.map((emp, i) => (
+                  <span key={i} className="badge">
+                    {emp.first_name} {emp.last_name}{" "}
+                    <i className="fas fa-trash-alt"></i>
+                  </span>
+                ))}
+              </div>
+            </div>
+          ) : (
+            ""
+          )}
+
+          {totalHoursEmployeeWeek ? (
+            <div className="alert alert-warning mt-3" role="alert">
+              <span>This employee have</span>{" "}
+              <strong>
+                {Math.round(totalHoursEmployeeWeek * 100) / 100 + "/40 hours "}
+              </strong>
+              <span>scheduled on this weeks payroll</span>
+            </div>
+          ) : null}
+          <div className="btn-bar">
+            {formData.status == "DRAFT" || formData.status == "UNDEFINED" ? ( // create shift
+              <button
+                type="button"
+                className="btn btn-secondary"
+                onClick={() => {
+                  if (!recurrent)
+                    formData.multiple_dates = multipleRecurrentShift;
+                  onSave({
+                    executed_action: isNaN(formData.id)
+                      ? "create_shift"
+                      : "update_shift",
+                    status: "DRAFT",
+                  });
+                }}
+              >
+                Save as draft
+              </button>
+            ) : (
+              ""
+            )}
+            {formData.status == "DRAFT" ? (
+              <button
+                type="button"
+                className="btn btn-success"
+                onClick={() => {
+                  if (!formData.has_sensitive_updates && !isNaN(formData.id))
+                    onSave({ executed_action: "update_shift", status: "OPEN" });
+                  else {
+                    const noti = Notify.info(
+                      "Are you sure? All talents will have to apply again the shift because the information was updated.",
+                      (answer) => {
+                        if (answer)
+                          onSave({
+                            executed_action: isNaN(formData.id)
+                              ? "create_shift"
+                              : "update_shift",
+                            status: "OPEN",
+                          });
+                        noti.remove();
+                      }
+                    );
+                  }
+                }}
+              >
+                Publish
+              </button>
+            ) : formData.status != "UNDEFINED" ? (
+              <button
+                type="button"
+                className="btn btn-primary"
+                onClick={() => {
+                  const noti = Notify.info(
+                    "Are you sure you want to unpublish this shift?",
+                    (answer) => {
+                      if (answer)
+                        onSave({
+                          executed_action: "update_shift",
+                          status: "DRAFT",
+                        });
+                      noti.remove();
+                    },
+                    9999999999999
+                  );
+                }}
+              >
+                Unpublish shift
+              </button>
+            ) : (
+              <button
+                type="button"
+                id="publish"
+                className="btn btn-primary"
+                onClick={() => {
+                  if (!recurrent) {
+                    saveRecurrentDates();
+                  } else {
+                    onSave({
+                      executed_action: isNaN(formData.id)
+                        ? "create_shift"
+                        : "update_shift",
+                      status: "OPEN",
+                    });
+                  }
+                }}
+              >
+                Save and publish
+              </button>
+            )}
+            {formData.status != "UNDEFINED" ? (
+              <button
+                type="button"
+                className="btn btn-danger"
+                onClick={() => {
+                  const noti = Notify.info(
+                    "Are you sure you want to cancel this shift?",
+                    (answer) => {
+                      if (answer)
+                        onSave({
+                          executed_action: "update_shift",
+                          status: "CANCELLED",
+                        });
+                      noti.remove();
+                    }
+                  );
+                }}
+              >
+                Delete
+              </button>
+            ) : (
+              ""
+            )}
+          </div>
+        </form>
+      </div>
+    </div>
+  );
+};
+EditOrAddShift.propTypes = {
+  error: PropTypes.string,
+  oldShift: PropTypes.object,
+  bar: PropTypes.object,
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+EditOrAddShift.defaultProps = {
+  oldShift: null,
+};
+
+/**
+ * ShiftDetails
+ */
+export const ShiftDetails = (props) => {
+  const creationMode = isNaN(props.formData.id);
+  // const shift = !props.catalog.shifts ? null : props.catalog.shifts.find(s => s.id == props.formData.id);
+  const shift = props.formData;
+  if (!creationMode && (!shift || typeof shift === "undefined"))
+    return <div>Loading shift...</div>;
+  return (
+    <Theme.Consumer>
+      {({ bar }) => (
+        <div>
+          {creationMode ? (
+            <EditOrAddShift bar={bar} {...props} />
+          ) : (
+            <div>
+              {!shift.expired ? (
+                <div className="top-bar">
+                  <Button
+                    icon="pencil"
+                    color="primary"
+                    size="small"
+                    rounded={true}
+                    note="Edit shift"
+                    notePosition="left"
+                    onClick={() =>
+                      props.onChange({ status: "DRAFT", hide_warnings: true })
+                    }
+                  />
+                  {["OPEN", "FILLED"].includes(shift.status) && (
+                    <Button
+                      icon="candidates"
+                      color="primary"
+                      size="small"
+                      rounded={true}
+                      onClick={() =>
+                        bar.show({
+                          slug: "show_shift_applications",
+                          data: shift,
+                          title: "Shift Applicants",
+                          allowLevels: true,
+                        })
+                      }
+                      note={
+                        shift.candidates.length > 0
+                          ? "The shift has applications that have not been reviewed"
+                          : "Shift Applicants"
+                      }
+                      withAlert={shift.candidates.length > 0}
+                      notePosition="left"
+                    />
+                  )}
+
+                  <Button
+                    icon="user_check"
+                    color="primary"
+                    notePosition="left"
+                    note="Shift accepted employees"
+                    size="small"
+                    rounded={true}
+                    onClick={() =>
+                      bar.show({
+                        slug: "show_shift_employees",
+                        data: shift,
+                        title: "Shift Employees",
+                        allowLevels: true,
+                      })
+                    }
+                  />
+                </div>
+              ) : (
+                <div className="top-bar">
+                  <Button
+                    icon="user_check"
+                    color="primary"
+                    notePosition="left"
+                    size="small"
+                    rounded={true}
+                    note="Shift accepted employees"
+                    onClick={() =>
+                      bar.show({
+                        slug: "show_shift_employees",
+                        data: shift,
+                        title: "Shift Employees",
+                        allowLevels: true,
+                      })
+                    }
+                  />
+                  <Button
+                    icon="dollar"
+                    color="primary"
+                    notePosition="left"
+                    size="small"
+                    rounded={true}
+                    note={
+                      shift.status !== "OPEN" ? (
+                        "Shift Payroll"
+                      ) : (
+                        <span>
+                          This shift is expired and the payroll has not been
+                          processed
+                        </span>
+                      )
+                    }
+                    withAlert={shift.status !== "OPEN"}
+                    onClick={() =>
+                      bar.show({
+                        slug: "select_timesheet",
+                        data: shift,
+                        allowLevels: true,
+                      })
+                    }
+                  />
+                </div>
+              )}
+              {props.formData.status === "DRAFT" ? (
+                <EditOrAddShift bar={bar} {...props} oldShift={shift} />
+              ) : (
+                <ShowShift bar={bar} shift={shift} />
+              )}
+
+              {moment(props.formData.ending_at).isBefore(NOW()) && (
+                <div className="row text-center mt-4">
+                  <div className="col">
+                    <Button
+                      color="primary"
+                      onClick={() =>
+                        bar.show({
+                          slug: "show_employees_rating",
+                          data: shift,
+                          allowLevels: true,
+                        })
+                      }
+                    >
+                      Rate Employees
+                    </Button>
+                  </div>
+                </div>
+              )}
+            </div>
+          )}
+        </div>
+      )}
+    </Theme.Consumer>
+  );
+};
+ShiftDetails.propTypes = {
+  error: PropTypes.string,
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  shift: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+const ShowShift = ({ shift, bar }) => {
+  const totalCandidates = Array.isArray(shift.candidates)
+    ? shift.candidates.length
+    : 0;
+  const totalEmployees = Array.isArray(shift.employees)
+    ? shift.employees.length
+    : 0;
+  const openVacancys = shift.maximum_allowed_employees - totalEmployees;
+  const startDate = shift.starting_at.format("ll");
+  const startTime = shift.starting_at.format("LT");
+  const endTime = shift.ending_at.format("LT");
+  return (
+    <div className="shift-details">
+      <h3>{"Shift details"}</h3>
+      {shift.status == "DRAFT" ? (
+        <span href="#" className="badge badge-secondary">
+          draft
+        </span>
+      ) : openVacancys == 0 ? (
+        <span href="#" className="badge" style={{ background: "#5cb85c" }}>
+          {totalEmployees} Filled
+        </span>
+      ) : (
+        <span href="#" className="badge badge-danger">
+          {totalCandidates}/{openVacancys}
+        </span>
+      )}
+      <a href="#" className="shift-position">
+        {shift.position.title}
+      </a>{" "}
+      @
+      <a href="#" className="shift-location">
+        {" "}
+        {shift.venue.title}
+      </a>
+      <span className="shift-date">
+        {" "}
+        {startDate} from {startTime} to {endTime}{" "}
+      </span>
+      {typeof shift.price == "string" ? (
+        <span className="shift-price"> ${shift.price}</span>
+      ) : (
+        <span className="shift-price">
+          {" "}
+          {shift.price.currencySymbol}
+          {shift.price.amount}
+        </span>
+      )}
+      <hr />
+      <div>
+        <ShiftEmployees catalog={{ shift: shift, showShift: true }} />
+      </div>
+      {/* <hr/>
+        <ShiftApplicants catalog={{shift: shift, applicants: shift.candidates, showShift: true}}/> */}
+    </div>
+  );
+};
+ShowShift.propTypes = {
+  shift: PropTypes.object.isRequired,
+  bar: PropTypes.object.isRequired,
+};
+ShowShift.defaultProps = {
+  shift: null,
+  bar: null,
+};
+
+/**
+ * RateShift
+ */
+export const RateShift = () => (
+  <div className="p-5 listcontents">
+    <div className="row">
+      <div className="col-12">
+        <h4>Venue name</h4>
+      </div>
+    </div>
+  </div>
+);
+RateShift.propTypes = {};
+
+/**
+ * RateShift
+ */
+export const ShiftTalentClockins = ({ formData, onChange, onSave }) => {
+  const { employee, clockins, shift } = formData;
+  const { bar } = useContext(Theme.Context);
+  const lastClockin =
+    clockins.length === 0
+      ? null
+      : moment.isMoment(clockins[clockins.length - 1].ended_at)
+      ? clockins[clockins.length - 1].ended_at
+      : moment(clockins[clockins.length - 1].ended_at);
+
+  return (
+    <div className="">
+      <div className="row">
+        <div className="col-12">
+          {/*<div className="top-bar">
+                    <Button size="small" color="primary" rounded={true} className="mr-2"
+                        onClick={() => onChange({ new_clocking: { started_at: null, ended_at: null, shift: shift.id } })}
+                    >add</Button>
+                </div>*/}
+          <h3>Clockins</h3>
+          {clockins.length == 0 && (
+            <p>
+              {employee.user.first_name} {employee.user.last_name} has not
+              clocked in to this shift yet
+            </p>
+          )}
+          {(clockins.length > 0 || formData.new_clocking) && (
+            <div className="row px-3 text-center">
+              <div className="col">In</div>
+              <div className="col">Out</div>
+            </div>
+          )}
+          {clockins.map((c) => {
+            let started_at = moment.isMoment(c.started_at)
+              ? c.started_at
+              : moment(c.started_at);
+            let ended_at = moment.isMoment(c.ended_at)
+              ? c.ended_at
+              : moment(c.ended_at);
+            return (
+              <div key={c.id} className="row px-3 text-center">
+                <div className="col">{started_at.format("LT")}</div>
+                <div className="col">
+                  {ended_at.isValid(c.ended_at) ? (
+                    ended_at.format("LT")
+                  ) : (
+                    <span className="badge badge-secondary">Still Working</span>
+                  )}
+                </div>
+              </div>
+            );
+          })}
+          {formData.new_clocking && (
+            <div className="row px-3 text-center">
+              <div className="col-6">
+                <TimePicker
+                  showSecond={false}
+                  defaultValue={lastClockin ? lastClockin : null}
+                  format={TIME_FORMAT}
+                  onChange={(value) =>
+                    onChange({
+                      new_clockin: {
+                        ...formData.new_clockin,
+                        started_at: value,
+                      },
+                    })
+                  }
+                  value={formData.new_clocking.started_at}
+                  use12Hours
+                  inputReadOnly
+                />
+              </div>
+              <div className="col-6">
+                <TimePicker
+                  showSecond={false}
+                  defaultValue={lastClockin ? lastClockin : null}
+                  format={TIME_FORMAT}
+                  onChange={(value) =>
+                    onChange({
+                      new_clockin: { ...formData.new_clockin, ended_at: value },
+                    })
+                  }
+                  value={formData.new_clocking.ended_at}
+                  use12Hours
+                  inputReadOnly
+                />
+              </div>
+              <div className="col-6 mt-2">
+                <Button
+                  color="primary"
+                  className="w-100"
+                  onClick={() =>
+                    onSave({
+                      executed_action: "add_clockin",
+                      clockin: formData.new_clocking,
+                    })
+                  }
+                >
+                  Save
+                </Button>
+              </div>
+              <div className="col-6 mt-2">
+                <Button
+                  color="secondary"
+                  className="w-100"
+                  onClick={() =>
+                    onChange({ formData: { ...formData, new_clockin: null } })
+                  }
+                >
+                  Cancel
+                </Button>
+              </div>
+            </div>
+          )}
+        </div>
+      </div>
+    </div>
+  );
+};
+ShiftTalentClockins.propTypes = {
+  formData: PropTypes.object.isRequired,
+  catalog: PropTypes.object.isRequired,
+  onChange: PropTypes.func,
+  onSave: PropTypes.func,
+  history: PropTypes.object.isRequired,
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_subscriptions.js.html b/docs/views_subscriptions.js.html new file mode 100644 index 0000000..273baa1 --- /dev/null +++ b/docs/views_subscriptions.js.html @@ -0,0 +1,236 @@ + + + + + JSDoc: Source: views/subscriptions.js + + + + + + + + + + +
+ +

Source: views/subscriptions.js

+ + + + + + +
+
+
import React, { useState, useEffect } from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from 'prop-types';
+import {store, createSubscription, searchMe } from '../actions.js';
+import { Button } from '../components/index';
+import {Notify} from 'bc-react-notifier';
+import { GET } from "../utils/api_wrapper.js";
+
+import Tooltip from 'rc-tooltip';
+import 'rc-tooltip/assets/bootstrap_white.css';
+
+//gets the queryst
+export const Subscription = (data) => {
+
+    const _defaults = {
+        id: '',
+        title: '',
+        unique_name: '',
+        serialize: function(){
+
+            const newLocation = {
+//                status: (this.status == 'UNDEFINED') ? 'DRAFT' : this.status,
+            };
+
+            return Object.assign(this, newLocation);
+        }
+    };
+
+    let _location = Object.assign(_defaults, data);
+    return {
+        validate: () => {
+            //if(validator.isEmpty(_location.title)) throw new ValidationError('The location title cannot be empty');
+            return _location;
+        },
+        defaults: () => {
+            return _defaults;
+        },
+        getFormData: () => {
+            const _formShift = {
+                //title: _location.title,
+            };
+            return _formShift;
+        }
+    };
+};
+
+/**
+ * YourSubscription
+ */
+const FeatureIndicator = ({fixed, additional, className, boolean}) => <div className={`m-0 ${className}`}>
+    { boolean !== null ?
+        <div>{boolean ? "Yes" : "No"}</div>
+        :
+        <div>
+            {fixed > 9999 ? "Unlimited" : fixed > 0 && `${fixed} / mo.`}
+            {fixed === 0 && additional > 0 ? 
+                "$" + additional + "/each"
+                : !additional || additional <=0 ? 
+                    '' 
+                    :
+                    <Tooltip placement="top" trigger={['hover']} overlay={<span>
+                        {fixed > 0 && fixed < 9999 && additional > 0 && " Plus "}
+                        {additional > 0 && "$" + additional + "/each"}
+                        {fixed > 0 && fixed < 9999 && additional > 0 && ` after the ${fixed} limit has been reached `}
+                    </span>}>
+                        <i className="fas fa-plus-circle ml-1"></i>
+                    </Tooltip>
+            }
+        </div>
+    }
+</div>;
+FeatureIndicator.propTypes = {
+  className: PropTypes.string,
+  boolean: PropTypes.bool,
+  fixed: PropTypes.number,
+  additional: PropTypes.number
+};
+FeatureIndicator.defaultProps = {
+  className: '',
+  fixed: 0,
+  boolean: null,
+  additional: 0
+};
+export const YourSubscription = (props) => {
+
+    const [ employer, setEmployer ] = useState(store.getState('current_employer'));
+    const [ plans, setPlans ] = useState([]);
+    const [ customer, setCustomer ] = useState('');
+    const [ subscription, setSubscription ] = useState('');
+
+    useEffect(() => {
+
+        const employerSubscription = store.subscribe('current_employer', (_employer) => setEmployer(_employer));
+        GET('subscriptions').then(subs => setPlans(subs));
+
+        searchMe('subscription').then(res => {
+            if(Array.isArray(res) && res[1 - res.length]){
+               
+                const cus = res[1 - res.length]['stripe_cus'];
+                const sub = res[1 - res.length]['stripe_sub'];
+
+                if(cus){
+                    setCustomer(res[1 - res.length]['stripe_cus']);
+                }
+                if(sub){
+                    setSubscription(res[1 - res.length]['stripe_sub']);
+                }
+            }
+        });
+
+        return () => {
+            employerSubscription.unsubscribe();
+        };
+    }, []);
+    
+    
+    
+    if(!employer) return "Loading";
+    return (<div>
+        <div className="row">
+            <div className="col-12">
+                Hello {employer.title}
+                <p>Current subscription: {employer.active_subscription ? employer.active_subscription.title : "No active subscription"}</p>
+            </div>
+        </div>
+        <div className="row">
+            <div className="col-3 text-center">
+                <h2>Features</h2>
+                <p className="m-0">Base Price</p>
+                <p className="m-0">Job Postings /mo.</p>
+                <p className="m-0">Maximum active employees / mo.</p>
+                <p className="m-0">Maximum Clock In/Out / mo.</p>
+                <p className="m-0">Talent Search	</p>
+                <p className="m-0">Rate Talent</p>
+                <p className="m-0">Invite Talent Manually</p>
+                <p className="m-0">Geo Clock In/Out report</p>
+                <p className="m-0">Payroll Reports</p>
+                <p className="m-0">Create favorite employees list size	</p>
+                <p className="m-0">Smart Calendar Features</p>
+                <p className="m-0">Pre Approved Trusted Talent</p>
+                <p className="m-0">On-line Payments</p>
+                <p className="m-0">Pre calculated deductions</p>
+                {/* <p className="m-0">QuickBooks Integration</p> */}
+            </div>
+            {plans.map(p => 
+                <div key={p.id} className="col-2 text-center">
+                    <h2>{p.title}</h2>
+                    <p className="m-0">${p.price_month}/month</p>
+                    <FeatureIndicator fixed={p.feature_max_shifts} additional={p.price_per_shifts} />
+                    <FeatureIndicator fixed={p.feature_max_active_employees} additional={p.price_per_active_employees} />
+                    <FeatureIndicator fixed={p.feature_max_clockins} additional={p.price_per_active_employees} />
+                    <FeatureIndicator boolean={p.feature_talent_search} />
+                    <FeatureIndicator boolean={true} />
+                    <FeatureIndicator fixed={p.feature_max_invites} additional={p.price_per_invites} />
+                    <FeatureIndicator boolean={true} />
+                    <FeatureIndicator boolean={p.feature_payroll_report} />
+                    <FeatureIndicator fixed={p.feature_max_favorite_list_size} />
+                    <FeatureIndicator boolean={p.feature_smart_calendar} />
+                    <FeatureIndicator boolean={p.feature_trusted_talents} />
+                    <FeatureIndicator boolean={p.feature_ach_payments} />
+                    <FeatureIndicator boolean={p.feature_calculate_deductions} />
+                    {/* <FeatureIndicator boolean={p.feature_quickbooks_integration} /> */}
+                    { (!employer.active_subscription || employer.active_subscription.id !== p.id) &&
+                        <Button className="w-100 mt-2" onClick={() => {
+                            const noti = Notify.info("Are you sure? You will lose any other subscription you may have", (answer) => {
+                                if (answer) createSubscription({ subscription: p.id, stripe_cus: customer, stripe_sub: subscription }, props.history);
+                                noti.remove();
+                            });
+                        }}>Apply</Button>
+                    }
+                </div>
+            )}
+        </div>
+        <div className="mt-4 pt-4">
+            <em>If you wish to cancel your subscription please contact us at support@jobcore.co</em>
+        </div>
+    </div>);
+};
+YourSubscription.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object //contains the data needed for the form to load
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/docs/views_talents.js.html b/docs/views_talents.js.html new file mode 100644 index 0000000..5f70b71 --- /dev/null +++ b/docs/views_talents.js.html @@ -0,0 +1,692 @@ + + + + + JSDoc: Source: views/talents.js + + + + + + + + + + +
+ +

Source: views/talents.js

+ + + + + + +
+
+
import React from "react";
+import Flux from "@4geeksacademy/react-flux-dash";
+import PropTypes from "prop-types";
+import { store, search, searchMe, fetchAllMe } from "../actions.js";
+import { callback, hasTutorial } from "../utils/tutorial";
+import {
+  EmployeeExtendedCard,
+  Avatar,
+  Stars,
+  Theme,
+  Button,
+  Wizard,
+} from "../components/index";
+import Select from "react-select";
+import queryString from "query-string";
+import { Session } from "bc-react-session";
+import { Notify } from "bc-react-notifier";
+import { ButtonGroup } from "reactstrap";
+import { useState, useEffect } from "react";
+import { Link } from "react-router-dom"
+//gets the querystring and creats a formData object to be used when opening the rightbar
+export const getTalentInitialFilters = (catalog) => {
+  let query = queryString.parse(window.location.search);
+  if (typeof query == "undefined") return {};
+  if (!Array.isArray(query.positions))
+    query.positions =
+      typeof query.positions == "undefined" ? [] : [query.positions];
+  if (!Array.isArray(query.badges))
+    query.badges = typeof query.badges == "undefined" ? [] : [query.badges];
+  return {
+    positions: query.positions.map((pId) =>
+      catalog.positions.find((pos) => pos.value == pId)
+    ),
+    badges: query.badges.map((bId) =>
+      catalog.badges.find((b) => b.value == bId)
+    ),
+    rating: catalog.stars.find((rate) => rate.value == query.rating),
+  };
+};
+
+export const Talent = (data) => {
+  const session = Session.getPayload();
+  const _defaults = {
+    //foo: 'bar',
+    serialize: function () {
+      const newShift = {
+        //foo: 'bar'
+        favoritelist_set: data.favoriteLists.map((fav) => fav.value),
+      };
+
+      return Object.assign(this, newShift);
+    },
+    unserialize: function () {
+      this.fullName = function () {
+        return this.user.first_name.length > 0
+          ? this.user.first_name + " " + this.user.last_name
+          : "No name specified";
+      };
+      if (typeof session.user.profile.employer != "undefined") {
+        if (typeof this.favoriteLists == "undefined")
+          this.favoriteLists = this.favoritelist_set.filter(
+            (fav) => fav.employer == session.user.profile.employer
+          );
+        else {
+          this.favoriteLists = this.favoritelist_set.map((fav) =>
+            store.get("favlists", fav.id || fav)
+          );
+        }
+      }
+
+      return this;
+    },
+  };
+
+  let _entity = Object.assign(_defaults, data);
+  return {
+    validate: () => {
+      return _entity;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _formShift = {
+        id: _entity.id,
+        favoriteLists: _entity.favoritelist_set.map((fav) => ({
+          label: fav.title,
+          value: fav.id,
+        })),
+      };
+      return _formShift;
+    },
+    filters: () => {
+      const _filters = {
+        positions: _entity.positions.map((item) => item.value),
+        rating:
+          typeof _entity.rating == "object" ? _entity.rating.value : undefined,
+        badges: _entity.badges.map((item) => item.value),
+      };
+      for (let key in _entity)
+        if (typeof _entity[key] == "function") delete _entity[key];
+      return Object.assign(_entity, _filters);
+    },
+  };
+};
+
+export const ShiftInvite = (data) => {
+  const user = Session.getPayload().user;
+  const _defaults = {
+    //foo: 'bar',
+    serialize: function () {
+      const newShiftInvite = {
+        //foo: 'bar'
+        sender: user.id,
+        shifts: data.shifts.map((s) => s.id || s.value.id),
+        employees: data.employees,
+      };
+
+      return Object.assign(this, newShiftInvite);
+    },
+  };
+
+  let _entity = Object.assign(_defaults, data);
+  return {
+    validate: () => {
+      return _entity;
+    },
+    defaults: () => {
+      return _defaults;
+    },
+    getFormData: () => {
+      const _formShift = {
+        employees: _entity.employees || [_entity.id],
+        shifts: _entity.shifts,
+      };
+      return _formShift;
+    },
+    filters: () => {
+      const _filters = {
+        //positions: _entity.positions.map( item => item.value ),
+      };
+      for (let key in _entity)
+        if (typeof _entity[key] == "function") delete _entity[key];
+      return Object.assign(_entity, _filters);
+    },
+  };
+};
+     
+export class ManageTalents extends Flux.DashView {
+  constructor() {
+    super();
+    this.state = {
+      // catalog: {this.state.catalog},
+      DocStatus: "",
+      empStatus: "unverified",
+      form: "",
+      formLoading: false,
+      employees: [],
+      runTutorial: hasTutorial(),
+      pagination: {
+        first: "",
+        last: "",
+        next: "",
+        previous: "",
+      },
+      steps: [
+        {
+          target: "#talent_search_header",
+          content: "In this page you can search our entire talent network",
+          placement: "right",
+        },
+        {
+          target: "#filter_talent",
+          content:
+            "Start by filtering by name, experience, badges or minium star rating",
+          placement: "left",
+        },
+      ],
+    };
+    this.handleStatusChange = this.handleStatusChange.bind(this);
+  }
+
+  componentDidMount() {
+    this.filter();
+    
+    const positions = store.getState("positions");
+
+    if (!positions) {
+      this.subscribe(store, "positions", (positions) => {
+        this.setState({ positions });
+      });
+    } else this.setState({ positions });
+
+    this.subscribe(store, "employees", (employees) => {
+      if (Array.isArray(employees) && employees.length !== 0)
+        this.setState({ employees });
+    });
+    const lists = store.getState("favlists");
+    // this.subscribe(store, 'favlists', (lists) => {
+    //     this.setState({ lists });
+    // });
+    if (!lists) fetchAllMe(["favlists"]);
+
+    this.props.history.listen(() => {
+      if (this.props.history.location.pathname == "/talents") this.filter("l");
+      else this.setState({ firstSearch: false });
+    });
+    this.setState({ runTutorial: true });
+    
+    this.handleStatusChange
+  }
+  componentWillUnmount() {
+    this.handleStatusChange
+  }
+  handleStatusChange() {
+    this.setState({ DocStatus: props.catalog.employee.employment_verification_status });
+  }
+  filter(url) {
+    // search('employees', window.location.search);
+    let queries = window.location.search;
+
+    if (queries) queries = "&" + queries.substring(1);
+    if (url && url.length > 50) {
+      const page = url.split("employees")[1];
+      if (page) {
+        search(`employees`, `${page + queries}`).then((data) => {
+          this.setState({
+            employees: data.results,
+            pagination: data,
+          });
+        });
+      } else null;
+    } else {
+      search(`employees`, `?envelope=true&limit=50${queries}`).then((data) => {
+        this.setState({
+          employees: data.results,
+          pagination: data,
+        });
+      });
+    }
+  }
+  
+  
+  render() {
+    const employees = this.state.employees
+    function checkEmployability(empl) {
+      const today = new Date()
+      const empDate = new Date(empl.employability_expired_at)
+      if (empl.employability_expired_at===null || empl.employability_expired_at===undefined || empDate.getTime()===0) {
+        return "The employee does not have a defined expiration date for employability"
+      } else if (empDate.getTime()<today.getTime()){
+          empl.employment_verification_status = "NOT_APPROVED"
+        return "Is NOT eligible to work"
+      } else {
+        return "It IS eligible to work"
+      }
+    }
+    const today = new Date()
+    //console.log("empleados#########", employees.map(checkEmployability)) // leave this one for now [Israel]
+    const positions = this.state.positions;
+    if (this.state.firstSearch) return <p>Please search for an employee</p>;
+    const allowLevels = window.location.search != "";
+    const filteredEmployeesList = (empStatus) => {
+      if (empStatus==="rejected") {
+        const notAprovedEmpList = employees.filter((employees) => employees.employment_verification_status==="NOT_APPROVED")
+        return  notAprovedEmpList
+      } else if (empStatus==="verified") {
+        const verifiedEmpList = employees.filter((employees) => employees.employment_verification_status==="APPROVED")
+        const sortedVerifiedEmpList = verifiedEmpList.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
+        return sortedVerifiedEmpList
+      } else {
+        const pendingEmpList = employees.filter((employees) => employees.employment_verification_status==="PENDING")
+        const beingReviewedEmpList = employees.filter((employees) => employees.employment_verification_status==="BEING_REVIEWED")
+        const missDocsEmpList = employees.filter((employees) => employees.employment_verification_status==="MISSING_DOCUMENTS")
+        const unverifiedEmpList = pendingEmpList.concat(beingReviewedEmpList, missDocsEmpList)
+        const sortedUnverifiedEmpList = unverifiedEmpList.slice().sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at))
+        return sortedUnverifiedEmpList
+      }
+    }
+    const finalList = filteredEmployeesList(this.state.empStatus)
+    return (
+      <div className="p-1 listcontents">
+        <Theme.Consumer>
+          {({ bar }) => (
+            <span>
+              {/* <Wizard continuous
+                      steps={this.state.steps}
+                      run={this.state.runTutorial}
+                      callback={callback}
+                    /> */}
+              <h1>
+                <span id="talent_search_header">Talent Search</span>
+              </h1>
+              <div className="my-2">
+                <button onClick={() => {this.setState({ empStatus: this.state.empStatus="rejected" }) 
+                }} type="button" className="btn btn-secondary" style={this.state.empStatus==="rejected" ? {background:"red"} : {}}>Rejected</button>
+                <button onClick={() => {this.setState({ empStatus: this.state.empStatus="verified" }) 
+                }} type="button" className="btn btn-secondary" style={this.state.empStatus==="verified" ? {background:"green"} : {}}>Verified</button>
+                <button onClick={() => {this.setState({ empStatus: this.state.empStatus="unverified" })
+                }} type="button" className="btn btn-secondary" style={this.state.empStatus==="unverified" ? {background:"#FFDB58", color:"gray"} : {}}>Unverified</button>
+              </div>
+              {// this.state.employees.map((s, i) => (
+                finalList.map((s, i) => (
+                <EmployeeExtendedCard
+                  form={this.state.form}
+                  formLoading={this.state.formLoading}
+                  key={i}
+                  employee={s}
+                  hover={true}
+                  positions={positions}
+                  onClick={(e) =>
+                    bar.show({ slug: "show_single_talent", data: s })
+                  }
+                  defineEmployee={(e) =>
+                    bar.show({ slug: "define_employee", data: s})
+                  }
+                >
+                  <Button
+                    className="btn btn-outline-dark"
+                    onClick={() =>
+                      bar.show({ slug: "add_to_favlist", data: s, allowLevels })
+                    }
+                  >
+                    Add to Favorites
+                  </Button>
+                  <Button
+                    className="btn btn-outline-dark"
+                    onClick={() =>
+                      bar.show({
+                        slug: "invite_talent_to_shift",
+                        data: s,
+                        allowLevels,
+                      })
+                    }
+                  >
+                    Invite
+                  </Button>
+                </EmployeeExtendedCard>
+              ))}
+              
+              <div className="row mt-4 justify-content-center">
+                <div className="col">
+                  <nav aria-label="Page navigation example">
+                    <ul className="pagination">
+                      {this.state.pagination.first && (
+                        <li className="page-item">
+                          <span
+                            className="page-link"
+                            aria-label="Previous"
+                            style={{ cursor: "pointer", color: "black" }}
+                            onClick={() =>
+                              this.filter(this.state.pagination.first)
+                            }
+                          >
+                            <span aria-hidden="true">
+                              <i className="fas fa-chevron-left"></i>
+                              <i className="fas fa-chevron-left"></i>
+                            </span>
+                            <span className="sr-only">{"First"}</span>
+                          </span>
+                        </li>
+                      )}
+                      {this.state.pagination.previous && (
+                        <li className="page-item">
+                          <span
+                            className="page-link"
+                            style={{ cursor: "pointer", color: "black" }}
+                            onClick={() =>
+                              this.filter(this.state.pagination.previous)
+                            }
+                          >
+                            <i className="fas fa-chevron-left"></i>
+                          </span>
+                        </li>
+                      )}
+                      {this.state.pagination.next && (
+                        <li className="page-item">
+                          <span
+                            className="page-link"
+                            style={{ cursor: "pointer", color: "black" }}
+                            onClick={() =>
+                              this.filter(this.state.pagination.next)
+                            }
+                          >
+                            <i className="fas fa-chevron-right"></i>
+                          </span>
+                        </li>
+                      )}
+
+                      {this.state.pagination.last && (
+                        <li className="page-item">
+                          <span
+                            className="page-link"
+                            onClick={() =>
+                              this.filter(this.state.pagination.last)
+                            }
+                            aria-label="Next"
+                            style={{ cursor: "pointer", color: "black" }}
+                          >
+                            <span aria-hidden="true">
+                              <i className="fas fa-chevron-right"></i>
+                              <i className="fas fa-chevron-right"></i>
+                            </span>
+                            <span className="sr-only">Last</span>
+                          </span>
+                        </li>
+                      )}
+                    </ul>
+                  </nav>
+                </div>
+              </div>
+            </span>
+          )}
+        </Theme.Consumer>
+        
+      </div>
+    );
+  }
+}
+
+/**
+ * AddShift
+ */
+export const FilterTalents = (props) => {
+  return (
+    <form>
+      <div className="row">
+        <div className="col-6">
+          <label>First Name:</label>
+          <input
+            className="form-control"
+            value={props.formData.first_name}
+            onChange={(e) => props.onChange({ first_name: e.target.value })}
+          />
+        </div>
+        <div className="col-6">
+          <label>Last Name:</label>
+          <input
+            className="form-control"
+            value={props.formData.last_name}
+            onChange={(e) => props.onChange({ last_name: e.target.value })}
+          />
+        </div>
+      </div>
+      <div className="row">
+        <div className="col-12">
+          <label>Experience in past positions:</label>
+          <Select
+            isMulti
+            value={props.formData.positions}
+            onChange={(selectedOption) =>
+              props.onChange({ positions: selectedOption })
+            }
+            options={props.catalog.positions}
+          />
+        </div>
+      </div>
+
+      {/* BADGE COMING SOON */}
+      {/* <div className="row">
+            <div className="col-12">
+                <label>Badges:</label>
+                <Select isMulti
+                    value={props.formData.badges}
+                    onChange={(selectedOption)=>props.onChange({badges: selectedOption})}
+                    options={props.catalog.badges}
+                />
+            </div>
+        </div> */}
+      <div className="row">
+        <div className="col-12">
+          <label>Minimum start rating</label>
+          <Select
+            value={props.formData.rating}
+            onChange={(opt) => props.onChange({ rating: opt })}
+            options={props.catalog.stars}
+          />
+        </div>
+      </div>
+      <div className="btn-bar">
+        <Button color="primary" onClick={() => props.onSave()}>
+          Apply Filters
+        </Button>
+        <Button
+          color="secondary"
+          onClick={() => {
+            props.formData.first_name = "";
+            props.formData.last_name = "";
+            props.formData.positions = [];
+            props.formData.badges = [];
+            props.formData.rating = "";
+            props.onSave(false);
+          }}
+        >
+          Clear Filters
+        </Button>
+      </div>
+    </form>
+  );
+};
+FilterTalents.propTypes = {
+  onSave: PropTypes.func.isRequired,
+  onCancel: PropTypes.func.isRequired,
+  onChange: PropTypes.func.isRequired,
+  formData: PropTypes.object,
+  catalog: PropTypes.object, //contains the data needed for the form to load
+};
+
+/**
+ * Talent Details
+ * 
+ * Before, the Stars component was rendered inside a p tag, 
+ * now its rendered inside a span tag
+ */
+export const TalentDetails = (props) => {
+  const employee = props.catalog.employee;
+
+  function reformatPhoneNumber(phoneNumberString) {
+    var cleaned = ("" + phoneNumberString).replace(/\D/g, "");
+    var match = cleaned.match(/^(1|)?(\d{3})(\d{3})(\d{4})$/);
+    if (match) {
+      var intlCode = match[1] ? "+1 " : "";
+      return [intlCode, "(", match[2], ") ", match[3], "-", match[4]].join("");
+    }
+    return null;
+  }
+              
+  return (
+    <Theme.Consumer>
+      {({ bar }) => (
+        <li className="aplication-details">
+          <div className="top-bar">
+            <Button
+              icon="envelope"
+              color="primary"
+              size="small"
+              rounded={true}
+              note="Pending Invites"
+              notePosition="left"
+              onClick={() =>
+                bar.show({
+                  slug: "show_talent_shift_invites",
+                  data: employee,
+                  allowLevels: true,
+                })
+              }
+            />
+          </div>
+          <Avatar url={employee.user.profile.picture} />
+          <span>
+            <Stars
+              rating={Number(employee.rating)}
+              jobCount={employee.total_ratings}
+            />
+          </span>
+          <p style={{ fontWeight: "bolder", fontSize: "24px" }}>
+            {typeof employee.fullName == "function"
+              ? employee.fullName()
+              : employee.first_name + " " + employee.last_name}
+          </p>
+          <p>
+            <strong>Email:</strong>{" "}
+            {employee.user ? employee.user.email : "No email provided"}
+          </p>
+          <p>
+            <strong>Phone number:</strong>{" "}
+            {employee.user && employee.user.profile.phone_number != ""
+              ? reformatPhoneNumber(employee.user.profile.phone_number)
+              : "none"}
+          </p>
+          <p>
+            <strong>Position(s):</strong>{" "}
+            {employee.positions.map((p) => p.title).join(", ")}
+          </p>
+          <p>
+            <strong>
+              ${Number(employee.minimum_hourly_rate).toFixed(2)}/hr minimum
+              expected rate
+            </strong>
+          </p>
+          <p>{employee.user.profile.bio}</p>
+
+          {/* {employee.positions.length > 0 && <p>{employee.positions.map(p => <ul key={p.id}><li className="badge badge-primary" style={{columnCount: 3}}>{p.title}</li></ul>)}</p>} */}
+          {employee.badges.length > 0 && (
+            <p>
+              {employee.badges.map((b) => (
+                <span key={b.id} className="badge badge-secondary">
+                  {b.title}
+                </span>
+              ))}
+            </p>
+          )}
+          <div className="btn-bar">
+            <Button
+              color="primary"
+              onClick={() =>
+                bar.show({
+                  slug: "invite_talent_to_shift",
+                  data: employee,
+                  allowLevels: true,
+                })
+              }
+            >
+              Invite to shift
+            </Button>
+            <Button
+              color="success"
+              onClick={() =>
+                bar.show({
+                  slug: "add_to_favlist",
+                  data: employee,
+                  allowLevels: true,
+                })
+              }
+            >
+              Add to favorites
+            </Button>
+          </div>
+          <div className="btn-bar">
+            <Button
+              color="danger"
+              onClick={() =>
+                
+                bar.show({
+                  slug: "check_employee_documents",
+                  data: employee,
+                  allowLevels: true,
+                })
+              }
+            >
+              Check employee documents
+            </Button>
+            </div>
+        </li>
+      )}
+    </Theme.Consumer>
+  );
+};
+TalentDetails.propTypes = {
+  catalog: PropTypes.object.isRequired,
+};
+
+
+
+ + + + +
+ + + +
+ +
+ Documentation generated by JSDoc 3.6.11 on Wed Oct 05 2022 17:57:01 GMT+0000 (Coordinated Universal Time) +
+ + + + + diff --git a/jsdoc-custom-template/README.md b/jsdoc-custom-template/README.md new file mode 100644 index 0000000..1946bef --- /dev/null +++ b/jsdoc-custom-template/README.md @@ -0,0 +1,12 @@ +The default template for JSDoc 3 uses: [the Taffy Database library](http://taffydb.com/) and the [Underscore Template library](http://underscorejs.org/). + + +## Generating Typeface Fonts + +The default template uses the [OpenSans](https://www.google.com/fonts/specimen/Open+Sans) typeface. The font files can be regenerated as follows: + +1. Open the [OpenSans page at Font Squirrel](). +2. Click on the 'Webfont Kit' tab. +3. Either leave the subset drop-down as 'Western Latin (Default)', or, if we decide we need more glyphs, than change it to 'No Subsetting'. +4. Click the 'DOWNLOAD @FONT-FACE KIT' button. +5. For each typeface variant we plan to use, copy the 'eot', 'svg' and 'woff' files into the 'templates/default/static/fonts' directory. diff --git a/jsdoc-custom-template/publish.js b/jsdoc-custom-template/publish.js new file mode 100644 index 0000000..522746b --- /dev/null +++ b/jsdoc-custom-template/publish.js @@ -0,0 +1,692 @@ +const doop = require('jsdoc/util/doop'); +const env = require('jsdoc/env'); +const fs = require('jsdoc/fs'); +const helper = require('jsdoc/util/templateHelper'); +const logger = require('jsdoc/util/logger'); +const path = require('jsdoc/path'); +const taffy = require('taffydb').taffy; +const template = require('jsdoc/template'); +const util = require('util'); + +const htmlsafe = helper.htmlsafe; +const linkto = helper.linkto; +const resolveAuthorLinks = helper.resolveAuthorLinks; +const hasOwnProp = Object.prototype.hasOwnProperty; + +let data; +let view; + +let outdir = path.normalize(env.opts.destination); + +function find(spec) { + return helper.find(data, spec); +} + +function tutoriallink(tutorial) { + return helper.toTutorial(tutorial, null, { + tag: 'em', + classname: 'disabled', + prefix: 'Tutorial: ' + }); +} + +function getAncestorLinks(doclet) { + return helper.getAncestorLinks(data, doclet); +} + +function hashToLink(doclet, hash) { + let url; + + if ( !/^(#.+)/.test(hash) ) { + return hash; + } + + url = helper.createLink(doclet); + url = url.replace(/(#.+|$)/, hash); + + return `${hash}`; +} + +function needsSignature({kind, type, meta}) { + let needsSig = false; + + // function and class definitions always get a signature + if (kind === 'function' || kind === 'class') { + needsSig = true; + } + // typedefs that contain functions get a signature, too + else if (kind === 'typedef' && type && type.names && + type.names.length) { + for (let i = 0, l = type.names.length; i < l; i++) { + if (type.names[i].toLowerCase() === 'function') { + needsSig = true; + break; + } + } + } + // and namespaces that are functions get a signature (but finding them is a + // bit messy) + else if (kind === 'namespace' && meta && meta.code && + meta.code.type && meta.code.type.match(/[Ff]unction/)) { + needsSig = true; + } + + return needsSig; +} + +function getSignatureAttributes({optional, nullable}) { + const attributes = []; + + if (optional) { + attributes.push('opt'); + } + + if (nullable === true) { + attributes.push('nullable'); + } + else if (nullable === false) { + attributes.push('non-null'); + } + + return attributes; +} + +function updateItemName(item) { + const attributes = getSignatureAttributes(item); + let itemName = item.name || ''; + + if (item.variable) { + itemName = `…${itemName}`; + } + + if (attributes && attributes.length) { + itemName = util.format( '%s%s', itemName, + attributes.join(', ') ); + } + + return itemName; +} + +function addParamAttributes(params) { + return params.filter(({name}) => name && !name.includes('.')).map(updateItemName); +} + +function buildItemTypeStrings(item) { + const types = []; + + if (item && item.type && item.type.names) { + item.type.names.forEach(name => { + types.push( linkto(name, htmlsafe(name)) ); + }); + } + + return types; +} + +function buildAttribsString(attribs) { + let attribsString = ''; + + if (attribs && attribs.length) { + attribsString = htmlsafe( util.format('(%s) ', attribs.join(', ')) ); + } + + return attribsString; +} + +function addNonParamAttributes(items) { + let types = []; + + items.forEach(item => { + types = types.concat( buildItemTypeStrings(item) ); + }); + + return types; +} + +function addSignatureParams(f) { + const params = f.params ? addParamAttributes(f.params) : []; + + f.signature = util.format( '%s(%s)', (f.signature || ''), params.join(', ') ); +} + +function addSignatureReturns(f) { + const attribs = []; + let attribsString = ''; + let returnTypes = []; + let returnTypesString = ''; + const source = f.yields || f.returns; + + // jam all the return-type attributes into an array. this could create odd results (for example, + // if there are both nullable and non-nullable return types), but let's assume that most people + // who use multiple @return tags aren't using Closure Compiler type annotations, and vice-versa. + if (source) { + source.forEach(item => { + helper.getAttribs(item).forEach(attrib => { + if (!attribs.includes(attrib)) { + attribs.push(attrib); + } + }); + }); + + attribsString = buildAttribsString(attribs); + } + + if (source) { + returnTypes = addNonParamAttributes(source); + } + if (returnTypes.length) { + returnTypesString = util.format( ' → %s{%s}', attribsString, returnTypes.join('|') ); + } + + f.signature = `${f.signature || ''}${returnTypesString}`; +} + +function addSignatureTypes(f) { + const types = f.type ? buildItemTypeStrings(f) : []; + + f.signature = `${f.signature || ''}${types.length ? ` :${types.join('|')}` : ''}`; +} + +function addAttribs(f) { + const attribs = helper.getAttribs(f); + const attribsString = buildAttribsString(attribs); + + f.attribs = util.format('%s', attribsString); +} + +function shortenPaths(files, commonPrefix) { + Object.keys(files).forEach(file => { + files[file].shortened = files[file].resolved.replace(commonPrefix, '') + // always use forward slashes + .replace(/\\/g, '/'); + }); + + return files; +} + +function getPathFromDoclet({meta}) { + if (!meta) { + return null; + } + + return meta.path && meta.path !== 'null' ? + path.join(meta.path, meta.filename) : + meta.filename; +} + +function generate(title, docs, filename, resolveLinks) { + let docData; + let html; + let outpath; + + resolveLinks = resolveLinks !== false; + + docData = { + env: env, + title: title, + docs: docs + }; + + outpath = path.join(outdir, filename); + html = view.render('container.tmpl', docData); + + if (resolveLinks) { + html = helper.resolveLinks(html); // turn {@link foo} into foo + } + + fs.writeFileSync(outpath, html, 'utf8'); +} + +function generateSourceFiles(sourceFiles, encoding = 'utf8') { + Object.keys(sourceFiles).forEach(file => { + let source; + // links are keyed to the shortened path in each doclet's `meta.shortpath` property + const sourceOutfile = helper.getUniqueFilename(sourceFiles[file].shortened); + + helper.registerLink(sourceFiles[file].shortened, sourceOutfile); + + try { + source = { + kind: 'source', + code: helper.htmlsafe( fs.readFileSync(sourceFiles[file].resolved, encoding) ) + }; + } + catch (e) { + logger.error('Error while generating source file %s: %s', file, e.message); + } + + generate(`Source: ${sourceFiles[file].shortened}`, [source], sourceOutfile, + false); + }); +} + +/** + * Look for classes or functions with the same name as modules (which indicates that the module + * exports only that class or function), then attach the classes or functions to the `module` + * property of the appropriate module doclets. The name of each class or function is also updated + * for display purposes. This function mutates the original arrays. + * + * @private + * @param {Array.} doclets - The array of classes and functions to + * check. + * @param {Array.} modules - The array of module doclets to search. + */ +function attachModuleSymbols(doclets, modules) { + const symbols = {}; + + // build a lookup table + doclets.forEach(symbol => { + symbols[symbol.longname] = symbols[symbol.longname] || []; + symbols[symbol.longname].push(symbol); + }); + + modules.forEach(module => { + if (symbols[module.longname]) { + module.modules = symbols[module.longname] + // Only show symbols that have a description. Make an exception for classes, because + // we want to show the constructor-signature heading no matter what. + .filter(({description, kind}) => description || kind === 'class') + .map(symbol => { + symbol = doop(symbol); + + if (symbol.kind === 'class' || symbol.kind === 'function') { + symbol.name = `${symbol.name.replace('module:', '(require("')}"))`; + } + + return symbol; + }); + } + }); +} + +function buildMemberNav(items, itemHeading, itemsSeen, linktoFn) { + let nav = ''; + + if (items.length) { + let itemsNav = ''; + + items.forEach(item => { + let displayName; + + if ( !hasOwnProp.call(item, 'longname') ) { + itemsNav += `
  • ${linktoFn('', item.name)}
  • `; + } + else if ( !hasOwnProp.call(itemsSeen, item.longname) ) { + if (env.conf.templates.default.useLongnameInNav) { + displayName = item.longname; + } else { + displayName = item.name; + } + itemsNav += `
  • ${linktoFn(item.longname, displayName.replace(/\b(module|event):/g, ''))}
  • `; + + itemsSeen[item.longname] = true; + } + }); + + if (itemsNav !== '') { + nav += `

    ${itemHeading}

      ${itemsNav}
    `; + } + } + + return nav; +} + +function linktoTutorial(longName, name) { + return tutoriallink(name); +} + +function linktoExternal(longName, name) { + return linkto(longName, name.replace(/(^"|"$)/g, '')); +} + +/** + * Create the navigation sidebar. + * @param {object} members The members that will be used to create the sidebar. + * @param {array} members.classes + * @param {array} members.externals + * @param {array} members.globals + * @param {array} members.mixins + * @param {array} members.modules + * @param {array} members.namespaces + * @param {array} members.tutorials + * @param {array} members.events + * @param {array} members.interfaces + * @return {string} The HTML for the navigation sidebar. + */ +function buildNav(members) { + let globalNav; + let nav = '

    JSDoc of employer-web-client

    '; + const seen = {}; + const seenTutorials = {}; + + nav += buildMemberNav(members.modules, 'Modules', {}, linkto); + nav += buildMemberNav(members.externals, 'Externals', seen, linktoExternal); + nav += buildMemberNav(members.namespaces, 'Namespaces', seen, linkto); + nav += buildMemberNav(members.classes, 'Classes', seen, linkto); + nav += buildMemberNav(members.interfaces, 'Interfaces', seen, linkto); + nav += buildMemberNav(members.events, 'Events', seen, linkto); + nav += buildMemberNav(members.mixins, 'Mixins', seen, linkto); + nav += buildMemberNav(members.tutorials, 'Tutorials', seenTutorials, linktoTutorial); + + if (members.globals.length) { + globalNav = ''; + + members.globals.forEach(({kind, longname, name}) => { + if ( kind !== 'typedef' && !hasOwnProp.call(seen, longname) ) { + globalNav += `
  • ${linkto(longname, name)}
  • `; + } + seen[longname] = true; + }); + + if (!globalNav) { + // turn the heading into a link so you can actually get to the global page + nav += `

    ${linkto('global', 'Global')}

    `; + } + else { + nav += `

    Global

      ${globalNav}
    `; + } + } + + return nav; +} + +/** + @param {TAFFY} taffyData See . + @param {object} opts + @param {Tutorial} tutorials + */ +exports.publish = (taffyData, opts, tutorials) => { + let classes; + let conf; + let externals; + let files; + let fromDir; + let globalUrl; + let indexUrl; + let interfaces; + let members; + let mixins; + let modules; + let namespaces; + let outputSourceFiles; + let packageInfo; + let packages; + const sourceFilePaths = []; + let sourceFiles = {}; + let staticFileFilter; + let staticFilePaths; + let staticFiles; + let staticFileScanner; + let templatePath; + + data = taffyData; + + conf = env.conf.templates || {}; + conf.default = conf.default || {}; + + templatePath = path.normalize(opts.template); + view = new template.Template( path.join(templatePath, 'tmpl') ); + + // claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness + // doesn't try to hand them out later + indexUrl = helper.getUniqueFilename('index'); + // don't call registerLink() on this one! 'index' is also a valid longname + + globalUrl = helper.getUniqueFilename('global'); + helper.registerLink('global', globalUrl); + + // set up templating + view.layout = conf.default.layoutFile ? + path.getResourcePath(path.dirname(conf.default.layoutFile), + path.basename(conf.default.layoutFile) ) : + 'layout.tmpl'; + + // set up tutorials for helper + helper.setTutorials(tutorials); + + data = helper.prune(data); + data.sort('longname, version, since'); + helper.addEventListeners(data); + + data().each(doclet => { + let sourcePath; + + doclet.attribs = ''; + + if (doclet.examples) { + doclet.examples = doclet.examples.map(example => { + let caption; + let code; + + if (example.match(/^\s*([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i)) { + caption = RegExp.$1; + code = RegExp.$3; + } + + return { + caption: caption || '', + code: code || example + }; + }); + } + if (doclet.see) { + doclet.see.forEach((seeItem, i) => { + doclet.see[i] = hashToLink(doclet, seeItem); + }); + } + + // build a list of source files + if (doclet.meta) { + sourcePath = getPathFromDoclet(doclet); + sourceFiles[sourcePath] = { + resolved: sourcePath, + shortened: null + }; + if (!sourceFilePaths.includes(sourcePath)) { + sourceFilePaths.push(sourcePath); + } + } + }); + + // update outdir if necessary, then create outdir + packageInfo = ( find({kind: 'package'}) || [] )[0]; + if (packageInfo && packageInfo.name) { + outdir = path.join( outdir, packageInfo.name, (packageInfo.version || '') ); + } + fs.mkPath(outdir); + + // copy the template's static files to outdir + fromDir = path.join(templatePath, 'static'); + staticFiles = fs.ls(fromDir, 3); + + staticFiles.forEach(fileName => { + const toDir = fs.toDir( fileName.replace(fromDir, outdir) ); + + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir); + }); + + // copy user-specified static files to outdir + if (conf.default.staticFiles) { + // The canonical property name is `include`. We accept `paths` for backwards compatibility + // with a bug in JSDoc 3.2.x. + staticFilePaths = conf.default.staticFiles.include || + conf.default.staticFiles.paths || + []; + staticFileFilter = new (require('jsdoc/src/filter').Filter)(conf.default.staticFiles); + staticFileScanner = new (require('jsdoc/src/scanner').Scanner)(); + + staticFilePaths.forEach(filePath => { + let extraStaticFiles; + + filePath = path.resolve(env.pwd, filePath); + extraStaticFiles = staticFileScanner.scan([filePath], 10, staticFileFilter); + + extraStaticFiles.forEach(fileName => { + const sourcePath = fs.toDir(filePath); + const toDir = fs.toDir( fileName.replace(sourcePath, outdir) ); + + fs.mkPath(toDir); + fs.copyFileSync(fileName, toDir); + }); + }); + } + + if (sourceFilePaths.length) { + sourceFiles = shortenPaths( sourceFiles, path.commonPrefix(sourceFilePaths) ); + } + data().each(doclet => { + let docletPath; + const url = helper.createLink(doclet); + + helper.registerLink(doclet.longname, url); + + // add a shortened version of the full path + if (doclet.meta) { + docletPath = getPathFromDoclet(doclet); + docletPath = sourceFiles[docletPath].shortened; + if (docletPath) { + doclet.meta.shortpath = docletPath; + } + } + }); + + data().each(doclet => { + const url = helper.longnameToUrl[doclet.longname]; + + if (url.includes('#')) { + doclet.id = helper.longnameToUrl[doclet.longname].split(/#/).pop(); + } + else { + doclet.id = doclet.name; + } + + if ( needsSignature(doclet) ) { + addSignatureParams(doclet); + addSignatureReturns(doclet); + addAttribs(doclet); + } + }); + + // do this after the urls have all been generated + data().each(doclet => { + doclet.ancestors = getAncestorLinks(doclet); + + if (doclet.kind === 'member') { + addSignatureTypes(doclet); + addAttribs(doclet); + } + + if (doclet.kind === 'constant') { + addSignatureTypes(doclet); + addAttribs(doclet); + doclet.kind = 'member'; + } + }); + + members = helper.getMembers(data); + members.tutorials = tutorials.children; + + // output pretty-printed source files by default + outputSourceFiles = conf.default && conf.default.outputSourceFiles !== false; + + // add template helpers + view.find = find; + view.linkto = linkto; + view.resolveAuthorLinks = resolveAuthorLinks; + view.tutoriallink = tutoriallink; + view.htmlsafe = htmlsafe; + view.outputSourceFiles = outputSourceFiles; + + // once for all + view.nav = buildNav(members); + attachModuleSymbols( find({ longname: {left: 'module:'} }), members.modules ); + + // generate the pretty-printed source files first so other pages can link to them + if (outputSourceFiles) { + generateSourceFiles(sourceFiles, opts.encoding); + } + + if (members.globals.length) { generate('Global', [{kind: 'globalobj'}], globalUrl); } + + // index page displays information from package.json and lists files + files = find({kind: 'file'}); + packages = find({kind: 'package'}); + + generate('JSDoc of employer-web-client', + packages.concat( + [{ + kind: 'mainpage', + readme: opts.readme, + longname: (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page' + }] + ).concat(files), indexUrl); + + // set up the lists that we'll use to generate pages + classes = taffy(members.classes); + modules = taffy(members.modules); + namespaces = taffy(members.namespaces); + mixins = taffy(members.mixins); + externals = taffy(members.externals); + interfaces = taffy(members.interfaces); + + Object.keys(helper.longnameToUrl).forEach(longname => { + const myClasses = helper.find(classes, {longname: longname}); + const myExternals = helper.find(externals, {longname: longname}); + const myInterfaces = helper.find(interfaces, {longname: longname}); + const myMixins = helper.find(mixins, {longname: longname}); + const myModules = helper.find(modules, {longname: longname}); + const myNamespaces = helper.find(namespaces, {longname: longname}); + + if (myModules.length) { + generate(`Module: ${myModules[0].name}`, myModules, helper.longnameToUrl[longname]); + } + + if (myClasses.length) { + generate(`Class: ${myClasses[0].name}`, myClasses, helper.longnameToUrl[longname]); + } + + if (myNamespaces.length) { + generate(`Namespace: ${myNamespaces[0].name}`, myNamespaces, helper.longnameToUrl[longname]); + } + + if (myMixins.length) { + generate(`Mixin: ${myMixins[0].name}`, myMixins, helper.longnameToUrl[longname]); + } + + if (myExternals.length) { + generate(`External: ${myExternals[0].name}`, myExternals, helper.longnameToUrl[longname]); + } + + if (myInterfaces.length) { + generate(`Interface: ${myInterfaces[0].name}`, myInterfaces, helper.longnameToUrl[longname]); + } + }); + + // TODO: move the tutorial functions to templateHelper.js + function generateTutorial(title, tutorial, filename) { + const tutorialData = { + title: title, + header: tutorial.title, + content: tutorial.parse(), + children: tutorial.children + }; + const tutorialPath = path.join(outdir, filename); + let html = view.render('tutorial.tmpl', tutorialData); + + // yes, you can use {@link} in tutorials too! + html = helper.resolveLinks(html); // turn {@link foo} into foo + + fs.writeFileSync(tutorialPath, html, 'utf8'); + } + + // tutorials can have only one parent so there is no risk for loops + function saveChildren({children}) { + children.forEach(child => { + generateTutorial(`Tutorial: ${child.title}`, child, helper.tutorialToUrl(child.name)); + saveChildren(child); + }); + } + + saveChildren(tutorials); +}; diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.eot new file mode 100644 index 0000000..5d20d91 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.svg new file mode 100644 index 0000000..3ed7be4 --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.woff new file mode 100644 index 0000000..1205787 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Bold-webfont.woff differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.eot new file mode 100644 index 0000000..1f639a1 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.svg new file mode 100644 index 0000000..6a2607b --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.woff new file mode 100644 index 0000000..ed760c0 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-BoldItalic-webfont.woff differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.eot new file mode 100644 index 0000000..0c8a0ae Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.svg new file mode 100644 index 0000000..e1075dc --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.svg @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.woff new file mode 100644 index 0000000..ff652e6 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Italic-webfont.woff differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.eot new file mode 100644 index 0000000..1486840 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.svg new file mode 100644 index 0000000..11a472c --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.woff new file mode 100644 index 0000000..e786074 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Light-webfont.woff differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.eot new file mode 100644 index 0000000..8f44592 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.svg new file mode 100644 index 0000000..431d7e3 --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.svg @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.woff new file mode 100644 index 0000000..43e8b9e Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-LightItalic-webfont.woff differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.eot b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.eot new file mode 100644 index 0000000..6bbc3cf Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.eot differ diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.svg b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.svg new file mode 100644 index 0000000..25a3952 --- /dev/null +++ b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.svg @@ -0,0 +1,1831 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.woff b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.woff new file mode 100644 index 0000000..e231183 Binary files /dev/null and b/jsdoc-custom-template/static/fonts/OpenSans-Regular-webfont.woff differ diff --git a/jsdoc-custom-template/static/scripts/linenumber.js b/jsdoc-custom-template/static/scripts/linenumber.js new file mode 100644 index 0000000..4354785 --- /dev/null +++ b/jsdoc-custom-template/static/scripts/linenumber.js @@ -0,0 +1,25 @@ +/*global document */ +(() => { + const source = document.getElementsByClassName('prettyprint source linenums'); + let i = 0; + let lineNumber = 0; + let lineId; + let lines; + let totalLines; + let anchorHash; + + if (source && source[0]) { + anchorHash = document.location.hash.substring(1); + lines = source[0].getElementsByTagName('li'); + totalLines = lines.length; + + for (; i < totalLines; i++) { + lineNumber++; + lineId = `line${lineNumber}`; + lines[i].id = lineId; + if (lineId === anchorHash) { + lines[i].className += ' selected'; + } + } + } +})(); diff --git a/jsdoc-custom-template/static/scripts/prettify/Apache-License-2.0.txt b/jsdoc-custom-template/static/scripts/prettify/Apache-License-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/jsdoc-custom-template/static/scripts/prettify/Apache-License-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/jsdoc-custom-template/static/scripts/prettify/lang-css.js b/jsdoc-custom-template/static/scripts/prettify/lang-css.js new file mode 100644 index 0000000..041e1f5 --- /dev/null +++ b/jsdoc-custom-template/static/scripts/prettify/lang-css.js @@ -0,0 +1,2 @@ +PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", +/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); diff --git a/jsdoc-custom-template/static/scripts/prettify/prettify.js b/jsdoc-custom-template/static/scripts/prettify/prettify.js new file mode 100644 index 0000000..eef5ad7 --- /dev/null +++ b/jsdoc-custom-template/static/scripts/prettify/prettify.js @@ -0,0 +1,28 @@ +var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; +(function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= +[],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), +l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, +q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, +q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, +"");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), +a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} +for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], +"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], +H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], +J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ +I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), +["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", +/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), +["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", +hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= +!k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p th:last-child { border-right: 1px solid #ddd; } + +.ancestors, .attribs { color: #999; } +.ancestors a, .attribs a +{ + color: #999 !important; + text-decoration: none; +} + +.clear +{ + clear: both; +} + +.important +{ + font-weight: bold; + color: #950B02; +} + +.yes-def { + text-indent: -1000px; +} + +.type-signature { + color: #aaa; +} + +.name, .signature { + font-family: Consolas, Monaco, 'Andale Mono', monospace; +} + +.details { margin-top: 14px; border-left: 2px solid #DDD; } +.details dt { width: 120px; float: left; padding-left: 10px; padding-top: 6px; } +.details dd { margin-left: 70px; } +.details ul { margin: 0; } +.details ul { list-style-type: none; } +.details li { margin-left: 30px; padding-top: 6px; } +.details pre.prettyprint { margin: 0 } +.details .object-value { padding-top: 0; } + +.description { + margin-bottom: 1em; + margin-top: 1em; +} + +.code-caption +{ + font-style: italic; + font-size: 107%; + margin: 0; +} + +.source +{ + border: 1px solid #ddd; + width: 80%; + overflow: auto; +} + +.prettyprint.source { + width: inherit; +} + +.source code +{ + font-size: 100%; + line-height: 18px; + display: block; + padding: 4px 12px; + margin: 0; + background-color: #fff; + color: #4D4E53; +} + +.prettyprint code span.line +{ + display: inline-block; +} + +.prettyprint.linenums +{ + padding-left: 70px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.prettyprint.linenums ol +{ + padding-left: 0; +} + +.prettyprint.linenums li +{ + border-left: 3px #ddd solid; +} + +.prettyprint.linenums li.selected, +.prettyprint.linenums li.selected * +{ + background-color: lightyellow; +} + +.prettyprint.linenums li * +{ + -webkit-user-select: text; + -moz-user-select: text; + -ms-user-select: text; + user-select: text; +} + +.params .name, .props .name, .name code { + color: #4D4E53; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 100%; +} + +.params td.description > p:first-child, +.props td.description > p:first-child +{ + margin-top: 0; + padding-top: 0; +} + +.params td.description > p:last-child, +.props td.description > p:last-child +{ + margin-bottom: 0; + padding-bottom: 0; +} + +.disabled { + color: #454545; +} diff --git a/jsdoc-custom-template/static/styles/prettify-jsdoc.css b/jsdoc-custom-template/static/styles/prettify-jsdoc.css new file mode 100644 index 0000000..5a2526e --- /dev/null +++ b/jsdoc-custom-template/static/styles/prettify-jsdoc.css @@ -0,0 +1,111 @@ +/* JSDoc prettify.js theme */ + +/* plain text */ +.pln { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* string content */ +.str { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a keyword */ +.kwd { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a comment */ +.com { + font-weight: normal; + font-style: italic; +} + +/* a type name */ +.typ { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a literal value */ +.lit { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* punctuation */ +.pun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp open bracket */ +.opn { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* lisp close bracket */ +.clo { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a markup tag name */ +.tag { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute name */ +.atn { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a markup attribute value */ +.atv { + color: #006400; + font-weight: normal; + font-style: normal; +} + +/* a declaration */ +.dec { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* a variable name */ +.var { + color: #000000; + font-weight: normal; + font-style: normal; +} + +/* a function name */ +.fun { + color: #000000; + font-weight: bold; + font-style: normal; +} + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; +} diff --git a/jsdoc-custom-template/static/styles/prettify-tomorrow.css b/jsdoc-custom-template/static/styles/prettify-tomorrow.css new file mode 100644 index 0000000..b6f92a7 --- /dev/null +++ b/jsdoc-custom-template/static/styles/prettify-tomorrow.css @@ -0,0 +1,132 @@ +/* Tomorrow Theme */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* Pretty printing styles. Used with prettify.js. */ +/* SPAN elements with the classes below are added by prettyprint. */ +/* plain text */ +.pln { + color: #4d4d4c; } + +@media screen { + /* string content */ + .str { + color: #718c00; } + + /* a keyword */ + .kwd { + color: #8959a8; } + + /* a comment */ + .com { + color: #8e908c; } + + /* a type name */ + .typ { + color: #4271ae; } + + /* a literal value */ + .lit { + color: #f5871f; } + + /* punctuation */ + .pun { + color: #4d4d4c; } + + /* lisp open bracket */ + .opn { + color: #4d4d4c; } + + /* lisp close bracket */ + .clo { + color: #4d4d4c; } + + /* a markup tag name */ + .tag { + color: #c82829; } + + /* a markup attribute name */ + .atn { + color: #f5871f; } + + /* a markup attribute value */ + .atv { + color: #3e999f; } + + /* a declaration */ + .dec { + color: #f5871f; } + + /* a variable name */ + .var { + color: #c82829; } + + /* a function name */ + .fun { + color: #4271ae; } } +/* Use higher contrast and text-weight for printable form. */ +@media print, projection { + .str { + color: #060; } + + .kwd { + color: #006; + font-weight: bold; } + + .com { + color: #600; + font-style: italic; } + + .typ { + color: #404; + font-weight: bold; } + + .lit { + color: #044; } + + .pun, .opn, .clo { + color: #440; } + + .tag { + color: #006; + font-weight: bold; } + + .atn { + color: #404; } + + .atv { + color: #060; } } +/* Style */ +/* +pre.prettyprint { + background: white; + font-family: Consolas, Monaco, 'Andale Mono', monospace; + font-size: 12px; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; } +*/ + +/* Specify class=linenums on a pre to get line numbering */ +ol.linenums { + margin-top: 0; + margin-bottom: 0; } + +/* IE indents via margin-left */ +li.L0, +li.L1, +li.L2, +li.L3, +li.L4, +li.L5, +li.L6, +li.L7, +li.L8, +li.L9 { + /* */ } + +/* Alternate shading for lines */ +li.L1, +li.L3, +li.L5, +li.L7, +li.L9 { + /* */ } diff --git a/jsdoc-custom-template/tmpl/augments.tmpl b/jsdoc-custom-template/tmpl/augments.tmpl new file mode 100644 index 0000000..446d28a --- /dev/null +++ b/jsdoc-custom-template/tmpl/augments.tmpl @@ -0,0 +1,10 @@ + + + +
      +
    • +
    + diff --git a/jsdoc-custom-template/tmpl/container.tmpl b/jsdoc-custom-template/tmpl/container.tmpl new file mode 100644 index 0000000..1b94004 --- /dev/null +++ b/jsdoc-custom-template/tmpl/container.tmpl @@ -0,0 +1,196 @@ + + + + + + + + + +
    + +
    + +

    + +

    + +
    + + + + +
    + + + +
    + +
    +
    + + +
    + + + + + + + + + +
    + + + + + +

    Example 1? 's':'' ?>

    + + + +
    + + +

    Extends

    + + + + + +

    Requires

    + +
      +
    • +
    + + + +

    Classes

    + +
    +
    +
    +
    + + + +

    Interfaces

    + +
    +
    +
    +
    + + + +

    Mixins

    + +
    +
    +
    +
    + + + +

    Namespaces

    + +
    +
    +
    +
    + + + +

    Members

    + + + + + + + +

    Methods

    + + + + + + + +

    Type Definitions

    + + + + + + + + + +

    Events

    + + + + + +
    + +
    + + + diff --git a/jsdoc-custom-template/tmpl/details.tmpl b/jsdoc-custom-template/tmpl/details.tmpl new file mode 100644 index 0000000..4a5bd49 --- /dev/null +++ b/jsdoc-custom-template/tmpl/details.tmpl @@ -0,0 +1,143 @@ +" + data.defaultvalue + ""; + defaultObjectClass = ' class="object-value"'; +} +?> + + +
    Properties:
    + + + + + +
    + + +
    Version:
    +
    + + + +
    Since:
    +
    + + + +
    Inherited From:
    +
    • + +
    + + + +
    Overrides:
    +
    • + +
    + + + +
    Implementations:
    +
      + +
    • + +
    + + + +
    Implements:
    +
      + +
    • + +
    + + + +
    Mixes In:
    + +
      + +
    • + +
    + + + +
    Deprecated:
    • Yes
    + + + +
    Author:
    +
    +
      +
    • +
    +
    + + + + + + + + +
    License:
    +
    + + + +
    Default Value:
    +
      + > +
    + + + +
    Source:
    +
    • + , +
    + + + +
    Tutorials:
    +
    +
      +
    • +
    +
    + + + +
    See:
    +
    +
      +
    • +
    +
    + + + +
    To Do:
    +
    +
      +
    • +
    +
    + +
    diff --git a/jsdoc-custom-template/tmpl/example.tmpl b/jsdoc-custom-template/tmpl/example.tmpl new file mode 100644 index 0000000..e87caa5 --- /dev/null +++ b/jsdoc-custom-template/tmpl/example.tmpl @@ -0,0 +1,2 @@ + +
    diff --git a/jsdoc-custom-template/tmpl/examples.tmpl b/jsdoc-custom-template/tmpl/examples.tmpl new file mode 100644 index 0000000..04d975e --- /dev/null +++ b/jsdoc-custom-template/tmpl/examples.tmpl @@ -0,0 +1,13 @@ + +

    + +
    + \ No newline at end of file diff --git a/jsdoc-custom-template/tmpl/exceptions.tmpl b/jsdoc-custom-template/tmpl/exceptions.tmpl new file mode 100644 index 0000000..9cef6c7 --- /dev/null +++ b/jsdoc-custom-template/tmpl/exceptions.tmpl @@ -0,0 +1,32 @@ + + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + Type +
    +
    + +
    +
    +
    +
    +
    + +
    + + + + + +
    + diff --git a/jsdoc-custom-template/tmpl/layout.tmpl b/jsdoc-custom-template/tmpl/layout.tmpl new file mode 100644 index 0000000..bc7e335 --- /dev/null +++ b/jsdoc-custom-template/tmpl/layout.tmpl @@ -0,0 +1,38 @@ + + + + + JSDoc: <?js= title ?> + + + + + + + + + + +
    + +

    + + +
    + + + +
    + +
    + Documentation generated by JSDoc on +
    + + + + + diff --git a/jsdoc-custom-template/tmpl/mainpage.tmpl b/jsdoc-custom-template/tmpl/mainpage.tmpl new file mode 100644 index 0000000..64e9e59 --- /dev/null +++ b/jsdoc-custom-template/tmpl/mainpage.tmpl @@ -0,0 +1,14 @@ + + + +

    + + + +
    +
    +
    + diff --git a/jsdoc-custom-template/tmpl/members.tmpl b/jsdoc-custom-template/tmpl/members.tmpl new file mode 100644 index 0000000..154c17b --- /dev/null +++ b/jsdoc-custom-template/tmpl/members.tmpl @@ -0,0 +1,38 @@ + +

    + + +

    + + + +
    + +
    + + + +
    Type:
    +
      +
    • + +
    • +
    + + + + + +
    Fires:
    +
      +
    • +
    + + + +
    Example 1? 's':'' ?>
    + + diff --git a/jsdoc-custom-template/tmpl/method.tmpl b/jsdoc-custom-template/tmpl/method.tmpl new file mode 100644 index 0000000..0125fe2 --- /dev/null +++ b/jsdoc-custom-template/tmpl/method.tmpl @@ -0,0 +1,131 @@ + + + +

    Constructor

    + + + +

    + + + +

    + + + + +
    + +
    + + + +
    Extends:
    + + + + +
    Type:
    +
      +
    • + +
    • +
    + + + +
    This:
    +
    + + + +
    Parameters:
    + + + + + + +
    Requires:
    +
      +
    • +
    + + + +
    Fires:
    +
      +
    • +
    + + + +
    Listens to Events:
    +
      +
    • +
    + + + +
    Listeners of This Event:
    +
      +
    • +
    + + + +
    Modifies:
    + 1) { ?>
      +
    • +
    + + + + +
    Throws:
    + 1) { ?>
      +
    • +
    + + + + +
    Returns:
    + 1) { ?>
      +
    • +
    + + + + +
    Yields:
    + 1) { ?>
      +
    • +
    + + + + +
    Example 1? 's':'' ?>
    + + diff --git a/jsdoc-custom-template/tmpl/modifies.tmpl b/jsdoc-custom-template/tmpl/modifies.tmpl new file mode 100644 index 0000000..16ccbf8 --- /dev/null +++ b/jsdoc-custom-template/tmpl/modifies.tmpl @@ -0,0 +1,14 @@ + + + +
    +
    + Type +
    +
    + +
    +
    + diff --git a/jsdoc-custom-template/tmpl/params.tmpl b/jsdoc-custom-template/tmpl/params.tmpl new file mode 100644 index 0000000..1fb4049 --- /dev/null +++ b/jsdoc-custom-template/tmpl/params.tmpl @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeAttributesDefaultDescription
    + + + + + + <optional>
    + + + + <nullable>
    + + + + <repeatable>
    + +
    + + + + +
    Properties
    + +
    diff --git a/jsdoc-custom-template/tmpl/properties.tmpl b/jsdoc-custom-template/tmpl/properties.tmpl new file mode 100644 index 0000000..40e0909 --- /dev/null +++ b/jsdoc-custom-template/tmpl/properties.tmpl @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameTypeAttributesDefaultDescription
    + + + + + + <optional>
    + + + + <nullable>
    + +
    + + + + +
    Properties
    +
    diff --git a/jsdoc-custom-template/tmpl/returns.tmpl b/jsdoc-custom-template/tmpl/returns.tmpl new file mode 100644 index 0000000..d070459 --- /dev/null +++ b/jsdoc-custom-template/tmpl/returns.tmpl @@ -0,0 +1,19 @@ + +
    + +
    + + + +
    +
    + Type +
    +
    + +
    +
    + \ No newline at end of file diff --git a/jsdoc-custom-template/tmpl/source.tmpl b/jsdoc-custom-template/tmpl/source.tmpl new file mode 100644 index 0000000..e559b5d --- /dev/null +++ b/jsdoc-custom-template/tmpl/source.tmpl @@ -0,0 +1,8 @@ + +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/jsdoc-custom-template/tmpl/tutorial.tmpl b/jsdoc-custom-template/tmpl/tutorial.tmpl new file mode 100644 index 0000000..88a0ad5 --- /dev/null +++ b/jsdoc-custom-template/tmpl/tutorial.tmpl @@ -0,0 +1,19 @@ +
    + +
    + 0) { ?> +
      +
    • +
    + + +

    +
    + +
    + +
    + +
    diff --git a/jsdoc-custom-template/tmpl/type.tmpl b/jsdoc-custom-template/tmpl/type.tmpl new file mode 100644 index 0000000..ec2c6c0 --- /dev/null +++ b/jsdoc-custom-template/tmpl/type.tmpl @@ -0,0 +1,7 @@ + + +| + \ No newline at end of file diff --git a/jsdoc-readme.md b/jsdoc-readme.md new file mode 100644 index 0000000..a14e947 --- /dev/null +++ b/jsdoc-readme.md @@ -0,0 +1,7 @@ +## Welcome to the documentation of JobCore's employer-web-client repo. + +### Here, you can explore the documentation of each function and block of code we have. + +### To navigate this documentation, please look at the list of names on the right side of the screen, under "Global." + +### There, you will find each documentation file. \ No newline at end of file diff --git a/jsdoc.json b/jsdoc.json new file mode 100644 index 0000000..45df6e4 --- /dev/null +++ b/jsdoc.json @@ -0,0 +1,18 @@ +{ + "source": { + "include": ["src"], + "includePattern": ".js$", + "excludePattern": "(node_modules/|docs)" + }, + "plugins": ["plugins/markdown"], + "templates": { + "cleverLinks": true, + "monospaceLinks": true + }, + "opts": { + "recurse": true, + "destination": "./docs/", + "template": "./jsdoc-custom-template", + "readme": "./jsdoc-readme.md" + } +} diff --git a/package-lock.json b/package-lock.json index 5955ff4..35f0458 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6099,6 +6099,25 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" }, + "@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==" + }, + "@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "requires": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -8615,9 +8634,9 @@ } }, "bluebird": { - "version": "3.5.5", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.5.tgz", - "integrity": "sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==" + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "bn.js": { "version": "4.11.8", @@ -9085,6 +9104,14 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "requires": { + "lodash": "^4.17.15" + } + }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", @@ -9114,6 +9141,11 @@ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", "dev": true }, + "chart.js": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" + }, "check-types": { "version": "11.1.2", "resolved": "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz", @@ -19533,12 +19565,59 @@ "esprima": "^2.6.0" } }, + "js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "requires": { + "xmlcreate": "^2.0.4" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, + "jsdoc": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", + "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", + "requires": { + "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.13.2" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + } + } + }, "jsdom": { "version": "19.0.0", "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-19.0.0.tgz", @@ -19748,6 +19827,14 @@ "is-buffer": "^1.1.5" } }, + "klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "requires": { + "graceful-fs": "^4.1.9" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -19805,6 +19892,14 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "requires": { + "uc.micro": "^1.0.1" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -20066,6 +20161,40 @@ "object-visit": "^1.0.0" } }, + "markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "requires": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==" + } + } + }, + "markdown-it-anchor": { + "version": "8.6.5", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.5.tgz", + "integrity": "sha512-PI1qEHHkTNWT+X6Ip9w+paonfIQ+QZP9sCeMYi47oqhH+EsW8CrJ8J7CzV19QVOj6il8ATGbK2nTECj22ZHGvQ==" + }, + "marked": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.1.0.tgz", + "integrity": "sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA==" + }, "math-expression-evaluator": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.3.7.tgz", @@ -20103,6 +20232,11 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==" }, + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" + }, "media-engine": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/media-engine/-/media-engine-1.0.3.tgz", @@ -26632,6 +26766,11 @@ "lodash.isequal": "^4.5.0" } }, + "react-chartjs-2": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz", + "integrity": "sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA==" + }, "react-cropper": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/react-cropper/-/react-cropper-2.1.4.tgz", @@ -26640,6 +26779,11 @@ "cropperjs": "^1.5.9" } }, + "react-date-object": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/react-date-object/-/react-date-object-2.1.5.tgz", + "integrity": "sha512-iAtF2nz+jZjy3w36qKx2MsxXqbiL7UgGljz9jPjLWxWCl4rDTZUMS/i7hKKiVbL1lJlmubz1Xu6W3i6CW06kTg==" + }, "react-datepicker": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.7.0.tgz", @@ -27522,6 +27666,11 @@ } } }, + "react-element-popper": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/react-element-popper/-/react-element-popper-2.1.6.tgz", + "integrity": "sha512-8va7mUmrKIkUnaM2t5Dyctd8cjgVgVcrv5vVD0FRay0sN6EPBCKa0bDi1/KmVDAjfgSIn7zQnjtc4VojcGrkgQ==" + }, "react-error-overlay": { "version": "6.0.10", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", @@ -27618,6 +27767,15 @@ "prop-types": "^15.7.2" } }, + "react-multi-date-picker": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/react-multi-date-picker/-/react-multi-date-picker-3.3.1.tgz", + "integrity": "sha512-W6SUBCULMgfNJstQx7BNzojkNdG9LVpOdYJIMjqTS1scWZ9BqG8nIIOhHUAObrTEdwz2S7eMxcDg6UeXe5D6uw==", + "requires": { + "react-date-object": "^2.1.5", + "react-element-popper": "^2.1.6" + } + }, "react-onclickoutside": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.7.1.tgz", @@ -32026,6 +32184,14 @@ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" }, + "requizzle": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", + "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", + "requires": { + "lodash": "^4.17.14" + } + }, "resolve": { "version": "1.22.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz", @@ -33648,6 +33814,11 @@ } } }, + "taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==" + }, "tapable": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.0.0.tgz", @@ -34159,6 +34330,11 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.17.tgz", "integrity": "sha512-uRdSdu1oA1rncCQL7sCj8vSyZkgtL7faaw9Tc9rZ3mGgraQ7+Pdx7w5mnOSF3gw9ZNG6oc+KXfkon3bKuROm0g==" }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" + }, "uglify-js": { "version": "3.3.16", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.3.16.tgz", @@ -34189,9 +34365,9 @@ } }, "underscore": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz", - "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg==" + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -36743,6 +36919,11 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, + "xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==" + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index aee5c2b..55d1bbd 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "deploy": "now --prod -b [API_HOST=https://jobcore.herokuapp.com]", "start:isratest": "API_HOST=https://8000-lavender-alligator-jdqsikyc39p.ws-us54.gitpod.io bash -c 'webpack-dev-server --config webpack.dev.js --open'", "test": "jest", - "test:coverage": "jest --coverage" + "test:coverage": "jest --coverage", + "doc": "jsdoc -c jsdoc.json" }, "author": "", "license": "ISC", @@ -96,6 +97,7 @@ "bc-react-notifier": "^1.1.6", "bc-react-session": "^1.5.2", "bootstrap": "^4.4.1", + "chart.js": "^3.9.1", "deepmerge": "^2.1.1", "downloadjs": "^1.4.7", "events": "^1.1.1", @@ -107,6 +109,7 @@ "interactjs": "^1.3.4", "jquery": "^3.4.1", "js-cookie": "^3.0.1", + "jsdoc": "^3.6.11", "lodash": "^4.17.15", "moment": "^2.22.2", "pdf-lib": "^1.16.0", @@ -118,6 +121,7 @@ "react": "^16.8.6", "react-ace": "^5.9.0", "react-calendar-timeline": "^0.22.0", + "react-chartjs-2": "^4.3.1", "react-cropper": "^2.1.4", "react-datepicker": "^4.7.0", "react-datetime": "^2.15.0", @@ -128,6 +132,7 @@ "react-dropzone": "^10.1.5", "react-joyride": "^2.2.1", "react-loader-spinner": "^3.1.14", + "react-multi-date-picker": "^3.3.1", "react-pdf": "^5.3.2", "react-places-autocomplete": "^7.2.0", "react-plaid-link": "^1.5.0", diff --git a/src/img/icons/metrics_color.png b/src/img/icons/metrics_color.png new file mode 100644 index 0000000..9a09c3b Binary files /dev/null and b/src/img/icons/metrics_color.png differ diff --git a/src/js/Layout.js b/src/js/Layout.js index 7f66f17..84182e2 100644 --- a/src/js/Layout.js +++ b/src/js/Layout.js @@ -36,6 +36,7 @@ class Layout extends Flux.View{ + diff --git a/src/js/PrivateLayout.js b/src/js/PrivateLayout.js index 95e3c4b..91f33c6 100644 --- a/src/js/PrivateLayout.js +++ b/src/js/PrivateLayout.js @@ -85,6 +85,7 @@ import { } from "./views/ratings.js"; import { PaymentsReport } from "./views/payments-report"; import { DeductionsReport } from "./views/deductions-report"; +import { Metrics } from "./views/metrics/metrics"; import { Profile, ManageUsers, @@ -110,7 +111,6 @@ import "bootstrap/dist/js/bootstrap.min.js"; import { CheckEmployeeDocuments2 } from "./views/check-documents2"; import Gleap from 'gleap'; - class PrivateLayout extends Flux.DashView { constructor() { super(); @@ -394,7 +394,7 @@ class PrivateLayout extends Flux.DashView { case "add_talent_to_favlist": option.title = "Search for the talent"; this.showRightBar(AddTalentToFavlist, option, { - formData: Favlist(option.data).getFormData(), + formData: Favlist(option.data).getFormData(), }); break; case "show_single_rating": @@ -644,8 +644,8 @@ class PrivateLayout extends Flux.DashView { level == "all" ? 0 : level == "last" - ? this.state.sideBarLevels.length - 1 - : parseInt(level, 10); + ? this.state.sideBarLevels.length - 1 + : parseInt(level, 10); // const lastLevel = this.state.sideBarLevels[this.state.sideBarLevels.length-1]; // if(Array.isArray(lastLevel.watchers)) lastLevel.watchers.forEach((w) => w.unsubscribe()); const newLevels = this.state.sideBarLevels.filter((e, i) => i < level); @@ -739,7 +739,7 @@ class PrivateLayout extends Flux.DashView { style={{ backgroundImage: `url(${logoURL})` }} /> ); - + return ( Profile +
  • + {showHideHR && ( + + + Metrics + + )} +

  • Help
  • */} -
  • - - +
  • + +
  • @@ -961,8 +969,8 @@ class PrivateLayout extends Flux.DashView { b.updated_at < a.updated_at ? -1 : b.updated_at > a.updated_at - ? 1 - : 0 + ? 1 + : 0 ) .map((emp, i) => { return ( @@ -1021,13 +1029,13 @@ class PrivateLayout extends Flux.DashView { {emp.ended_at ? "clocked out at " + - moment(emp.ended_at).format( - "MM/DD/YYYY, hh:mm A" - ) + moment(emp.ended_at).format( + "MM/DD/YYYY, hh:mm A" + ) : "clocked in at " + - moment(emp.started_at).format( - "MM/DD/YYYY, hh:mm A" - )} + moment(emp.started_at).format( + "MM/DD/YYYY, hh:mm A" + )} {" "} to shift{" "} @@ -1046,7 +1054,7 @@ class PrivateLayout extends Flux.DashView { "LT" )}{" "} {typeof emp.shift.price == - "string" ? ( + "string" ? ( {" "} ${emp.shift.price} @@ -1084,7 +1092,7 @@ class PrivateLayout extends Flux.DashView { "LT" )}{" "} {typeof emp.shift.price == - "string" ? ( + "string" ? ( {" "} ${emp.shift.price} @@ -1148,7 +1156,7 @@ class PrivateLayout extends Flux.DashView { /> - + + - - + this.state.bar.show(option)} /> @@ -1233,11 +1242,11 @@ class PrivateLayout extends Flux.DashView { ); } hideComponent(admin) { - if (admin==="a@jobcore.co") { - // this.setState({ showHideHR: false }); // uncommenting makes tap talent search is visible only to JC-HR@admin.co + if (admin === "a@jobcore.co") { + // this.setState({ showHideHR: false }); // When this is uncommented, it makes both "talent search" and "metrics" visible only to JC-HR@admin.co console.log("showHideHR###", this.state.showHideHR) console.log("admin###", admin) - } + } } } export default PrivateLayout; diff --git a/src/js/actions.js b/src/js/actions.js index 6fd7e40..979784f 100644 --- a/src/js/actions.js +++ b/src/js/actions.js @@ -15,6 +15,7 @@ import log from "./utils/log"; import WEngine from "./utils/write_engine.js"; import qs from "query-string"; import { normalizeToSnakeCase } from "./utils/validation"; + const Models = { shifts: Shift, ratings: Rating, @@ -55,23 +56,23 @@ export const autoLogin = (token = "") => { Notify.error(error.message || error); log.error(error); }) - ); + ); }; export const stripeStatus = (email, password, keep, history, id) => { - GET('subscription_auth/'+ email).then( - function(value) { - Notify.success("Welcome!") - - }, - function(reason) { - history.push("/subscribe") - Notify.error("Your subscription is not active, please get a new one") + GET('subscription_auth/' + email).then( + function (value) { + Notify.success("Welcome!") + + }, + function (reason) { + history.push("/subscribe") + Notify.error("Your subscription is not active, please get a new one") } - ) - -} + ) + +} export const login = (email, password, keep, history, id) => { -new Promise((resolve, reject) => + new Promise((resolve, reject) => POST("login", { username_or_email: email, password: password, @@ -79,9 +80,9 @@ new Promise((resolve, reject) => exp_days: keep ? 30 : 1, }) .then( - setTimeout(() => {stripeStatus(email, password, keep, history, id)}, 1000) - ) - .then(function (data) { + setTimeout(() => { stripeStatus(email, password, keep, history, id) }, 1000) + ) + .then(function (data) { // if (Number(data.user.profile.employer) != Number(id)) { // let company = data.user.profile.other_employers.find(emp => emp.employer == Number(id) ); // updateCompanyUser({id: company.profile_id, employer: company.employer, employer_role: company.employer_role}, { 'Authorization': 'JWT ' + data.token }); @@ -119,7 +120,7 @@ new Promise((resolve, reject) => reject(error.message || error); Notify.error(error.message || error); log.error(error); - + }) ); } @@ -144,7 +145,7 @@ export const signup = (formData, history) => }) .then(function (data) { Notify.success("You have signed up successfully! You are being redirected to the login screen"); - setTimeout(() => {history.push(`/login?type=${formData.account_type}`)}, 2500) + setTimeout(() => { history.push(`/login?type=${formData.account_type}`) }, 2500) resolve(); }) .catch(function (error) { @@ -152,7 +153,7 @@ export const signup = (formData, history) => Notify.error(error.message || error); log.error(error); }) - }); + }); export const remind = (email) => new Promise((resolve, reject) => @@ -227,13 +228,13 @@ export const sendCompanyInvitation = (email, employer, employer_role, sender) => new Promise((resolve, reject) => POST( "user/email/company/send/" + - email + - "/" + - sender + - "/" + - employer + - "/" + - employer_role, + email + + "/" + + sender + + "/" + + employer + + "/" + + employer_role, { email: email, sender: sender, @@ -447,7 +448,7 @@ export const search = (entity, queryString = null) => new Promise((accept, reject) => GET(entity, queryString) .then(function (list) { - console.log("list", list); + //console.log("list", list); if (typeof entity.callback == "function") entity.callback(); Flux.dispatchEvent(entity.slug || entity, list); accept(list); @@ -482,7 +483,7 @@ export const create = (entity, data, status = WEngine.modes.LIVE) => new Promise((resolve, reject) => { POST("employers/me/" + (entity.url || entity), data) .then(function (incoming) { - console.log("incoming", incoming); + //console.log("incoming", incoming); if ( typeof entity.url === "string" && typeof entity.slug === "undefined" @@ -511,19 +512,19 @@ export const create = (entity, data, status = WEngine.modes.LIVE) => Object.assign({ ...data, id: inc.id }) ); } - console.log("slug", entity.slug); - console.log("entity", entity); - console.log("entities", entities); - console.log("newShifts", newShifts); + //console.log("slug", entity.slug); + //console.log("entity", entity); + //console.log("entities", entities); + //console.log("newShifts", newShifts); Flux.dispatchEvent(entity.slug || entity, entities.concat(newShifts)); } Notify.success( "The " + - (entity.slug || entity).substring( - 0, - (entity.slug || entity).length - 1 - ) + - " was created successfully" + (entity.slug || entity).substring( + 0, + (entity.slug || entity).length - 1 + ) + + " was created successfully" ); resolve(incoming); }) @@ -579,8 +580,8 @@ export const remove = (entity, data) => { const name = path.split("/"); Notify.success( "The " + - name[0].substring(0, name[0].length - 1) + - " was deleted successfully" + name[0].substring(0, name[0].length - 1) + + " was deleted successfully" ); }) .catch(function (error) { @@ -633,69 +634,69 @@ export const updateProfileMe = (data) => { }; export const updateEmployability = (data) => { - PUT(`employee/employability_expired_at/update/${data.catalog.employee.id}`, - data) - .then() + PUT(`employee/employability_expired_at/update/${data.catalog.employee.id}`, + data) + .then() } export const updateDocs = (data) => { - PUT(`employee/employment_verification_status/update/${data.catalog.employee.id}`, - data) - .then(response => response.json()) - .then(data => console.log(data)) + PUT(`employee/employment_verification_status/update/${data.catalog.employee.id}`, + data) + .then(response => response.json()) + .then(data => console.log(data)) } export const createSubscription = (data, history) => { const employer = store.getState("current_employer"); - - POST(`employers/me/subscription`, data) - .then(function (active_subscription) { - - Flux.dispatchEvent("current_employer", { - ...employer, - active_subscription, - }); - Notify.success("The subscription was created successfully"); - - - }).then( - setTimeout(() => {history.push("/welcome")}, 4000) - - ) - .catch(function (error) { - console.log("ERROR", error); - Notify.error(error.message || error); - log.error(error); - }) - + + POST(`employers/me/subscription`, data) + .then(function (active_subscription) { + + Flux.dispatchEvent("current_employer", { + ...employer, + active_subscription, + }); + Notify.success("The subscription was created successfully"); + + + }).then( + setTimeout(() => { history.push("/welcome") }, 4000) + + ) + .catch(function (error) { + console.log("ERROR", error); + Notify.error(error.message || error); + log.error(error); + }) + }; export const createStripePayment2 = async () => { const response = await POSTcsrf2('create-payment-single-emp') - .then( - Notify.success("The payment was received successfully") - ) - .catch(function (error) { - console.log("ERROR", error); - Notify.error(error.message || error); - log.error(error); - }) + .then( + Notify.success("The payment was received successfully") + ) + .catch(function (error) { + console.log("ERROR", error); + Notify.error(error.message || error); + log.error(error); + }) + + return response - return response - }; export const createStripePayment = async (stripeToken) => { const response = await POSTcsrf('create-payment-intent', stripeToken) - .then( - Notify.success("The payment was received successfully") - ) - .catch(function (error) { - console.log("ERROR", error); - Notify.error(error.message || error); - log.error(error); - }) + .then( + Notify.success("The payment was received successfully") + ) + .catch(function (error) { + console.log("ERROR", error); + Notify.error(error.message || error); + log.error(error); + }) + + return response - return response - }; export const updateSubscription = (data, history) => { @@ -951,8 +952,7 @@ export const updateTalentList = (action, employee, listId) => { }) ); Notify.success( - `The talent was successfully ${ - action == "add" ? "added" : "removed" + `The talent was successfully ${action == "add" ? "added" : "removed" }` ); resolve(updatedFavlist); @@ -1037,9 +1037,9 @@ export const makeEmployeePayment = ( paymentType === "CHECK" ? {} : { - employer_bank_account_id: employer_bank_account_id, - employee_bank_account_id: employee_bank_account_id, - }, + employer_bank_account_id: employer_bank_account_id, + employee_bank_account_id: employee_bank_account_id, + }, deductions_list: deductions_list, deductions: deductions, }; @@ -1183,10 +1183,10 @@ class _Store extends Flux.DashStore { !Array.isArray(clockins) ? [] : clockins.map((c) => ({ - ...c, - started_at: moment(c.starting_at), - ended_at: moment(c.ended_at), - })) + ...c, + started_at: moment(c.starting_at), + ended_at: moment(c.ended_at), + })) ); this.addEvent("jobcore-invites"); this.addEvent("ratings", (_ratings) => @@ -1216,26 +1216,26 @@ class _Store extends Flux.DashStore { applicants.constructor === Object) ? [] : applicants.map((app) => { - app.shift = Shift(app.shift).defaults().unserialize(); - return app; - }); + app.shift = Shift(app.shift).defaults().unserialize(); + return app; + }); }); this.addEvent("shifts", (shifts) => { shifts = Array.isArray(shifts.results) ? shifts.results : Array.isArray(shifts) - ? shifts - : null; + ? shifts + : null; let newShifts = !shifts || - (Object.keys(shifts).length === 0 && shifts.constructor === Object) + (Object.keys(shifts).length === 0 && shifts.constructor === Object) ? [] : shifts - .filter((s) => s.status !== "CANCELLED") - .map((shift) => { - //already transformed - return Shift(shift).defaults().unserialize(); - }); + .filter((s) => s.status !== "CANCELLED") + .map((shift) => { + //already transformed + return Shift(shift).defaults().unserialize(); + }); const applicants = this.getState("applications"); if (!applicants && Session.get().isValid) fetchAllMe(["applications"]); @@ -1267,8 +1267,8 @@ class _Store extends Flux.DashStore { ) ? employer.payroll_period_starting_time : employer.payroll_period_starting_time - ? moment(employer.payroll_period_starting_time) - : moment(employer.created_at).startOf("isoWeek"); + ? moment(employer.payroll_period_starting_time) + : moment(employer.created_at).startOf("isoWeek"); return employer; }); this.addEvent("single_payroll_detail", (payroll) => { @@ -1277,17 +1277,17 @@ class _Store extends Flux.DashStore { let paid = true; payroll.clockins = !clockins || - (Object.keys(clockins).length === 0 && clockins.constructor === Object) + (Object.keys(clockins).length === 0 && clockins.constructor === Object) ? [] : clockins.map((clockin) => { - //already transformed - if (clockin.status == "PENDING") { - approved = false; - paid = false; - } else if (clockin.status != "PAID") paid = false; + //already transformed + if (clockin.status == "PENDING") { + approved = false; + paid = false; + } else if (clockin.status != "PAID") paid = false; - return Clockin(clockin).defaults().unserialize(); - }); + return Clockin(clockin).defaults().unserialize(); + }); if (typeof payroll.talent != "undefined") payroll.talent.paymentsApproved = approved; diff --git a/src/js/components/applicant-card/EmployeeExtendedCard.jsx b/src/js/components/applicant-card/EmployeeExtendedCard.jsx index 9b68ae9..219552e 100644 --- a/src/js/components/applicant-card/EmployeeExtendedCard.jsx +++ b/src/js/components/applicant-card/EmployeeExtendedCard.jsx @@ -447,7 +447,7 @@ const EmployeeExtendedCard = (props) => {
    { - console.log("button hovered"); + //console.log("button hovered"); props.defineEmployee() }} > @@ -461,8 +461,8 @@ const EmployeeExtendedCard = (props) => { data-target="#exampleModalCenter" onClick={(e) => { e.stopPropagation(); - console.log("button clicked"); - console.log("props dentro", props); + //console.log("button clicked"); + //console.log("props dentro", props); // Notify.info("Press ESC to close this window") // if (!props.formLoading) getEmployeeDocumet(props, "w4"); bar.show({ diff --git a/src/js/csrftoken.js b/src/js/csrftoken.js index 70e6d98..6bd0b41 100644 --- a/src/js/csrftoken.js +++ b/src/js/csrftoken.js @@ -39,7 +39,7 @@ const CSRFToken = () => { // CSRFToken() setcsrftoken(Cookies.get('csrftoken')); - console.log("este es el useeffect") + //console.log("este es el useeffect") }, []); diff --git a/src/js/utils/api_wrapper.js b/src/js/utils/api_wrapper.js index 2a44662..7e5c152 100644 --- a/src/js/utils/api_wrapper.js +++ b/src/js/utils/api_wrapper.js @@ -65,9 +65,6 @@ const appendCompany = (data) => { */ export const GET = async (endpoint, queryString = null, extraHeaders = {}) => { let url = `${rootAPIendpoint}/${endpoint}`; - console.log("GET###") - console.log("endpoint###", endpoint) - console.log("url###", url) if (queryString) url += queryString; HEADERS['Authorization'] = `JWT ${getToken()}`; @@ -89,7 +86,6 @@ export const GET = async (endpoint, queryString = null, extraHeaders = {}) => { }; export const POST = (endpoint, postData, extraHeaders = {}) => { - console.log("POST###") if (['user/register', 'login', 'user/password/reset','employers/me/jobcore-invites'].indexOf(endpoint) == -1) { HEADERS['Authorization'] = `JWT ${getToken()}`; postData = appendCompany(postData); @@ -145,7 +141,6 @@ export const POST = (endpoint, postData, extraHeaders = {}) => { // var headers = new Headers(); // headers.append('X-CSRFToken', csrftoken); export const POSTcsrf = (endpoint, postData, extraHeaders = {}) => { - console.log("POST###") // Cookies.get('csrftoken') // console.log("postData###", postData) Cookies.set('stripetoken', postData.id) @@ -160,7 +155,6 @@ export const POSTcsrf = (endpoint, postData, extraHeaders = {}) => { body: JSON.stringify(postData), // mode: 'no-cors' }; - console.log("REQ###", REQ) const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ) .then((resp) => processResp(resp, req)) .then(data => resolve(data)) @@ -175,7 +169,6 @@ export const POSTcsrf = (endpoint, postData, extraHeaders = {}) => { }; export const POSTcsrf2 = (endpoint, postData, extraHeaders = {}) => { - console.log("POST###") // Cookies.get('csrftoken') // console.log("postData###", postData) // Cookies.set('stripetoken', postData.id) @@ -190,7 +183,6 @@ export const POSTcsrf2 = (endpoint, postData, extraHeaders = {}) => { body: JSON.stringify(postData), // mode: 'no-cors' }; - console.log("REQ###", REQ) const req = new Promise((resolve, reject) => fetch(`${rootAPIendpoint}/${endpoint}`, REQ) .then((resp) => processResp(resp, req)) .then(data => resolve(data)) @@ -211,7 +203,6 @@ export const POSTcsrf2 = (endpoint, postData, extraHeaders = {}) => { // credentials: 'include' // }). export const PUTFiles = (endpoint, files) => { - console.log("PUTfiles###") const headers = { 'Authorization': `JWT ${getToken()}` }; @@ -239,7 +230,6 @@ export const PUTFiles = (endpoint, files) => { }; export const PUT = (endpoint, putData, extraHeaders = {}) => { - console.log("PUT###") if (['register', 'login','user/password/reset'].indexOf(endpoint) == -1) { HEADERS['Authorization'] = `JWT ${getToken()}`; } @@ -263,7 +253,6 @@ export const PUT = (endpoint, putData, extraHeaders = {}) => { }; export const DELETE = (endpoint, extraHeaders = {}) => { - console.log("DELETE###") HEADERS['Authorization'] = `JWT ${getToken()}`; const REQ = { @@ -284,7 +273,6 @@ export const DELETE = (endpoint, extraHeaders = {}) => { }; const processResp = function (resp, req = null) { - console.log(resp) PendingReq.remove(req); if (resp.ok) { if (resp.status == 204) return new Promise((resolve, reject) => resolve(true)); diff --git a/src/js/views/auth.js b/src/js/views/auth.js index 98a52f9..f2f87aa 100644 --- a/src/js/views/auth.js +++ b/src/js/views/auth.js @@ -158,9 +158,9 @@ export class Login extends React.Component { onClick={() => { // this.toggleInverted this.setState({hidePassword: !this.state.hidePassword}) - console.log("dentro del onClick#######") + //console.log("dentro del onClick#######") - console.log("this.state.shown#######", this.state.hidePassword) + //console.log("this.state.shown#######", this.state.hidePassword) }}> { /> */} {/*
    */} - + @@ -543,9 +543,9 @@ export class Signup extends React.Component { onClick={() => { // this.toggleInverted this.setState({hidePassword: !this.state.hidePassword}) - console.log("dentro del onClick#######") + //console.log("dentro del onClick#######") - console.log("this.state.shown#######", this.state.hidePassword) + //console.log("this.state.shown#######", this.state.hidePassword) }}> diff --git a/src/js/views/favorites.js b/src/js/views/favorites.js index 9986ef2..a09b9c7 100644 --- a/src/js/views/favorites.js +++ b/src/js/views/favorites.js @@ -175,8 +175,6 @@ export const AddFavlistsToTalent = ({ onCancel, catalog, }) => { - console.log(formData); - console.log(catalog); return ( {({ bar }) => ( diff --git a/src/js/views/metrics/charts.js b/src/js/views/metrics/charts.js new file mode 100644 index 0000000..3414094 --- /dev/null +++ b/src/js/views/metrics/charts.js @@ -0,0 +1,62 @@ +import React from 'react'; +import { Pie, Bar } from 'react-chartjs-2'; +import { Chart as ChartJS } from 'chart.js/auto'; + +/** + * @function + * @description Creates a pie chart with the data passed as an argument. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires Pie + * @param {object} pieData - Object with data like colors, labels, and values needed for the chart. + */ +export const PieChart = ({ pieData }) => { + return ( + + ) +} + +/** + * @function + * @description Creates a bar chart with the data passed as an argument + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires Bar + * @param {object} barData - Object with data like colors, labels, and values needed for the chart. + */ +export const BarChart = ({ barData }) => { + + let delayed; + return ( + { + delayed = true; + }, + delay: (context) => { + let delay = 0; + if (context.type === 'data' && context.mode === 'default' && !delayed) { + delay = context.dataIndex * 150 + context.datasetIndex * 100; + } + return delay; + }, + } + }} + /> + ) +} \ No newline at end of file diff --git a/src/js/views/metrics/dummy-data/ShiftsData.js b/src/js/views/metrics/dummy-data/ShiftsData.js new file mode 100644 index 0000000..71cd062 --- /dev/null +++ b/src/js/views/metrics/dummy-data/ShiftsData.js @@ -0,0 +1,570 @@ +export const ShiftsData = [ + { + id: 1, + employees: [5, 4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:00:00Z", + ended_at: "2022-10-28T15:00:00Z", + automatically_closed: false, + employee: 5, + shift: 1 + }, + { + started_at: "2022-10-28T15:00:00Z", + ended_at: "2022-10-28T20:10:00Z", + automatically_closed: false, + employee: 5, + shift: 1 + }, + { + started_at: "2022-10-28T10:05:00Z", + ended_at: "2022-10-28T15:00:00Z", + automatically_closed: false, + employee: 4, + shift: 1 + }, + { + started_at: "2022-10-28T15:00:00Z", + ended_at: "2022-10-28T20:15:00Z", + automatically_closed: false, + employee: 4, + shift: 1 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 2, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:30:09Z", + ended_at: "2022-10-28T13:00:00Z", + automatically_closed: false, + employee: 5, + shift: 2 + }, + { + started_at: "2022-10-28T13:00:00Z", + ended_at: "2022-10-28T20:25:00Z", + automatically_closed: false, + employee: 5, + shift: 2 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 3, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:00:00Z", + ended_at: "2022-10-28T12:07:00Z", + automatically_closed: false, + employee: 4, + shift: 3 + }, + { + started_at: "2022-10-28T12:20:00Z", + ended_at: "2022-10-28T17:00:00Z", + automatically_closed: false, + employee: 4, + shift: 3 + }, + { + started_at: "2022-10-28T17:00:00Z", + ended_at: "2022-10-28T20:20:00Z", + automatically_closed: false, + employee: 4, + shift: 3 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 4, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T15:30:09Z", + automatically_closed: false, + employee: 5, + shift: 4 + }, + { + started_at: "2022-10-28T15:42:09Z", + ended_at: "2022-10-28T18:00:09Z", + automatically_closed: false, + employee: 5, + shift: 4 + }, + { + started_at: "2022-10-28T18:30:09Z", + ended_at: "2022-10-28T20:31:09Z", + automatically_closed: false, + employee: 5, + shift: 4 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 5, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:00:09Z", + ended_at: "2022-10-28T20:19:09Z", + automatically_closed: true, + employee: 4, + shift: 5 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 6, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:40:09Z", + ended_at: "2022-10-28T19:25:09Z", + automatically_closed: false, + employee: 4, + shift: 6 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 7, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T14:10:09Z", + automatically_closed: false, + employee: 5, + shift: 7 + }, + { + started_at: "2022-10-28T14:28:09Z", + ended_at: "2022-10-28T19:20:09Z", + automatically_closed: false, + employee: 5, + shift: 7 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 8, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:14:09Z", + ended_at: "2022-10-28T16:30:09Z", + automatically_closed: false, + employee: 5, + shift: 8 + }, + { + started_at: "2022-10-28T17:00:09Z", + ended_at: "2022-10-28T21:35:09Z", + automatically_closed: false, + employee: 5, + shift: 8 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 9, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:08:09Z", + ended_at: "2022-10-28T15:00:09Z", + automatically_closed: false, + employee: 4, + shift: 9 + }, + { + started_at: "2022-10-28T15:36:09Z", + ended_at: "2022-10-28T20:00:09Z", + automatically_closed: false, + employee: 4, + shift: 9 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 10, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:10:09Z", + ended_at: "2022-10-28T14:00:09Z", + automatically_closed: false, + employee: 5, + shift: 10 + }, + { + started_at: "2022-10-28T14:40:09Z", + ended_at: "2022-10-28T20:40:09Z", + automatically_closed: false, + employee: 5, + shift: 10 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 11, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T08:00:09Z", + ended_at: "2022-10-28T13:00:09Z", + automatically_closed: false, + employee: 5, + shift: 11 + }, + { + started_at: "2022-10-28T13:45:09Z", + ended_at: "2022-10-28T20:45:09Z", + automatically_closed: false, + employee: 5, + shift: 11 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 12, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T12:00:09Z", + automatically_closed: false, + employee: 4, + shift: 12 + }, + { + started_at: "2022-10-28T12:34:09Z", + ended_at: "2022-10-28T20:00:09Z", + automatically_closed: false, + employee: 4, + shift: 12 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 13, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T22:00:09Z", + automatically_closed: true, + employee: 5, + shift: 13 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T22:00:00Z" + }, + { + id: 14, + employees: [4, 5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T22:00:09Z", + automatically_closed: false, + employee: 4, + shift: 14 + }, + { + started_at: "2022-10-28T09:10:09Z", + ended_at: "2022-10-28T22:00:09Z", + automatically_closed: true, + employee: 5, + shift: 14 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T20:00:00Z" + }, + { + id: 15, + employees: [4], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:10:09Z", + ended_at: "2022-10-28T12:00:09Z", + automatically_closed: false, + employee: 4, + shift: 15 + }, + { + started_at: "2022-10-28T12:30:09Z", + ended_at: "2022-10-28T22:00:09Z", + automatically_closed: true, + employee: 4, + shift: 15 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T22:00:00Z" + }, + { + id: 16, + employees: [5], + status: "COMPLETED", + clockin: [ + { + started_at: "2022-10-28T10:05:09Z", + ended_at: "2022-10-28T12:00:09Z", + automatically_closed: false, + employee: 5, + shift: 16 + }, + { + started_at: "2022-10-28T12:30:09Z", + ended_at: "2022-10-28T22:00:09Z", + automatically_closed: false, + employee: 5, + shift: 16 + } + ], + starting_at: "2022-10-28T10:00:00Z", + ending_at: "2022-10-28T22:00:00Z" + }, + { + id: 17, + status: "FILLED", + clockin: [], + employees: [2] + }, + { + id: 18, + status: "FILLED", + clockin: [], + employees: [1] + }, + { + id: 19, + status: "FILLED", + clockin: [], + employees: [8] + }, + { + id: 20, + status: "FILLED", + clockin: [], + employees: [9] + }, + { + id: 21, + status: "FILLED", + clockin: [], + employees: [15] + }, + { + id: 22, + status: "FILLED", + clockin: [], + employees: [13] + }, + { + id: 23, + status: "FILLED", + clockin: [], + employees: [1] + }, + { + id: 24, + status: "FILLED", + clockin: [], + employees: [8] + }, + { + id: 25, + status: "FILLED", + clockin: [], + employees: [11] + }, + { + id: 26, + status: "FILLED", + clockin: [], + employees: [2] + }, + { + id: 27, + status: "FILLED", + clockin: [], + employees: [10] + }, + { + id: 28, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 29, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 30, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 31, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 32, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 33, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 34, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 35, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 36, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 37, + status: "EXPIRED", + clockin: [], + employees: [] + }, + { + id: 38, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 39, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 40, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 41, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 42, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 43, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 44, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 45, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 46, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 47, + status: "OPEN", + clockin: [], + employees: [] + }, + { + id: 48, + status: "OPEN", + clockin: [], + employees: [] + } + ]; + \ No newline at end of file diff --git a/src/js/views/metrics/dummy-data/WorkersData.js b/src/js/views/metrics/dummy-data/WorkersData.js new file mode 100644 index 0000000..ee2ca79 --- /dev/null +++ b/src/js/views/metrics/dummy-data/WorkersData.js @@ -0,0 +1,203 @@ +export const WorkersData = [ + { + id: 1, + user: { + first_name: "Andrea", + last_name: "Villa", + profile: { + picture: "" + } + }, + created_at: "2022-09-28T23:12:48.00Z", + rating: 4 + }, + { + id: 2, + user: { + first_name: "Maria", + last_name: "Cuevas", + profile: { + picture: "" + } + }, + created_at: "2022-08-27T23:12:48.00Z", + rating: 4 + }, + { + id: 3, + user: { + first_name: "Eric", + last_name: "Schumer", + profile: { + picture: "" + } + }, + rating: 5, + total_ratings: 0, + created_at: "2022-09-02T23:12:48.00Z", + employment_verification_status: "APPROVED" + }, + { + id: 4, + user: { + first_name: "Hans", + last_name: "Zimmer", + profile: { + picture: "" + } + }, + rating: 3, + total_ratings: 0, + created_at: "2022-08-25T23:12:48.00Z", + employment_verification_status: "APPROVED" + }, + { + id: 5, + user: { + first_name: "John", + last_name: "Doe", + profile: { + picture: "" + } + }, + rating: 4, + total_ratings: 0, + created_at: "2022-08-29T23:12:48.00Z", + employment_verification_status: "APPROVED" + }, + { + id: 6, + user: { + first_name: "Alexander", + last_name: "Smith", + profile: { + picture: "" + } + }, + created_at: "2022-08-27T23:12:48.00Z", + rating: 4 + }, + { + id: 7, + user: { + first_name: "Des", + last_name: "Maxwell", + profile: { + picture: "" + } + }, + rating: 2, + total_ratings: 0, + created_at: "2022-09-01T23:12:48.00Z", + employment_verification_status: "APPROVED" + }, + { + id: 8, + user: { + first_name: "Estella", + last_name: "Reyes", + profile: { + picture: "" + } + }, + created_at: "2022-08-26T23:12:48.00Z", + rating: 3 + }, + { + id: 9, + user: { + first_name: "Henry", + last_name: "Stevens", + profile: { + picture: "" + } + }, + created_at: "2022-08-30T23:12:48.00Z", + rating: 2 + }, + { + id: 10, + user: { + first_name: "Cacia", + last_name: "Alvarez", + profile: { + picture: "" + } + }, + created_at: "2022-09-26T23:12:48.00Z", + rating: 2 + }, + { + id: 11, + user: { + first_name: "Esteban", + last_name: "Rodriguez", + profile: { + picture: "" + } + }, + created_at: "2022-09-29T23:12:48.00Z", + rating: null + }, + { + id: 12, + user: { + first_name: "Jessie", + last_name: "Simmons", + profile: { + picture: "" + } + }, + created_at: "2022-09-30T23:12:48.00Z", + rating: 1 + }, + { + id: 13, + user: { + first_name: "Erika", + last_name: "Maxwell", + profile: { + picture: "" + } + }, + created_at: "2021-10-01T23:12:48.00Z", + rating: 5 + }, + { + id: 14, + user: { + first_name: "Juan", + last_name: "Ponce", + profile: { + picture: "" + } + }, + created_at: "2021-10-02T23:12:48.00Z", + rating: 1 + }, + { + id: 15, + user: { + first_name: "Julio", + last_name: "Perez", + profile: { + picture: "" + } + }, + created_at: "2021-10-03T23:12:48.00Z", + rating: 2 + }, + { + id: 16, + user: { + first_name: "Carlos", + last_name: "Ocacio", + profile: { + picture: "" + } + }, + created_at: "2022-09-27T23:12:48.00Z", + rating: null + } + ]; + \ No newline at end of file diff --git a/src/js/views/metrics/general-stats/GeneralStats.js b/src/js/views/metrics/general-stats/GeneralStats.js new file mode 100644 index 0000000..6ad3c3b --- /dev/null +++ b/src/js/views/metrics/general-stats/GeneralStats.js @@ -0,0 +1,62 @@ +import React from "react"; +import { JobSeekers } from "./JobSeekers/JobSeekers"; +import { Hours } from "./Hours/Hours"; +import { Shifts } from "./Shifts/Shifts"; + +/** + * @function + * @description Creates a page with 3 tabs that show metrics about Shifts, Punctuality, and Hours. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires Hours + * @requires Shifts + * @requires JobSeekers + * @param {object} props - Contains an array of all shifts, and an array of all the workers. + */ +export const GeneralStats = (props) => { + + // Setting up main data sources + let workers = props.workers + let shifts = props.shifts + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    + {/* Tabs Controller Starts */} + + {/* Tabs Controller Ends */} + + {/* Tabs Content Starts */} + + {/* Tabs Content Ends */} +
    + ) +} diff --git a/src/js/views/metrics/general-stats/Hours/Hours.js b/src/js/views/metrics/general-stats/Hours/Hours.js new file mode 100644 index 0000000..dc7afef --- /dev/null +++ b/src/js/views/metrics/general-stats/Hours/Hours.js @@ -0,0 +1,500 @@ +import React, { useState, useEffect } from "react"; +import { PieChart } from "../../charts"; +import { HoursDataGenerator } from "./HoursData"; +import weekends from "react-multi-date-picker/plugins/highlight_weekends" +import DatePicker from "react-multi-date-picker"; +import moment from "moment"; + +/** + * @function + * @description Creates a page with a table and a graph of the hours worked and their trends. + * @since 10.28.22 by Paola Sanchez + * @author Paola Sanchez + * @requires PieChart + * @requires HoursDataGenerator + * @param {object} props - Contains an array of all the shifts. + */ +export const Hours = (props) => { + + /* Variables --------------------------------------------------- */ + + /* Putting props into variable */ + let shiftProps = props.shifts; + + /* Start of given time period */ + const [start, setStart] = useState(moment().format("MM-DD-YYYY")); + + /* End of given time period */ + const [end, setEnd] = useState(moment().format("MM-DD-YYYY")); + + /* Variables to store selected time period */ + const [period, setPeriod] = useState(); + + /* Variables to store selected dates */ + const [day, setDay] = useState(new Date()); + const [week, setWeek] = useState([ + new Date(), + new Date() + ]) + const [month, setMonth] = useState(new Date()); + const [year, setYear] = useState(new Date()); + + /* Variables to store date defaults*/ + const [defaultDay, setDefaultDay] = useState(new Date().toLocaleDateString()); + const [defaultWeek, setDefaultWeek] = useState( + new Date().toLocaleDateString() + ); + + const [defaultMonth, setDefaultMonth] = useState( + new Date().toLocaleDateString("en-us", { month: "long", year: "numeric" }) + ); + + const [defaultYear, setDefaultYear] = useState(new Date().getFullYear()); + + /* Handlers --------------------------------------------------- */ + + /* Day Handler */ + const handleDay = (day) => { + setDay(day); + setDefaultDay(day); + }; + + /* Week Handler */ + const handleWeek = (week) => { + setWeek(week); + setDefaultWeek(week); + }; + + /* Month Handler */ + const handleMonth = (month) => { + setMonth(month); + setDefaultMonth(month); + }; + + /* Year Handler */ + const handleYear = (year) => { + setYear(year); + setDefaultYear(year); + }; + + /* Calculate start and end of day */ + const startEndOfDay = () => { + // Transforming day value into string + let dayAsString = defaultDay.toString(); + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const dayFormatted = dayAsString.split(search).join(replaceWith); + + // Setting start and end + setStart(dayFormatted); + setEnd(dayFormatted); + }; + + /* Calculate start and end of week */ + const startEndOfWeek = () => { + // Transforming week value into string + let weekAsString = defaultWeek.toString(); + + let weeks = weekAsString.split(","); + + let endOfWeekPlaceholder = moment().isoWeekday(7).format("MM-DD-YYYY"); + + if (weeks.length === 1) { + weeks.push(endOfWeekPlaceholder); + } + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const weekStart = weeks[0].split(search).join(replaceWith); + const weekEnd = weeks[1].split(search).join(replaceWith); + + let weekStartF = moment(weekEnd).isoWeekday(1).format("MM-DD-YYYY"); + let weekEndF = moment(weekEnd).isoWeekday(7).format("MM-DD-YYYY"); + + // Setting start and end + setStart(weekStartF); + setEnd(weekEndF); + }; + + /* Calculate start and end of month */ + const startEndOfMonth = () => { + // Transforming month value into string + let monthAsString = defaultMonth.toString(); + + // Listing all months by number + const months = [ + { name: "January", number: "01" }, + { name: "February", number: "02" }, + { name: "March", number: "03" }, + { name: "April", number: "04" }, + { name: "May", number: "05" }, + { name: "June", number: "06" }, + { name: "July", number: "07" }, + { name: "August", number: "08" }, + { name: "September", number: "09" }, + { name: "October", number: "10" }, + { name: "November", number: "11" }, + { name: "December", number: "12" } + ]; + + // Determining year (number) of month value + let yearOfMonth = monthAsString.slice(-4); + + // Determining month (number) of month value + let monthOfMonth = ""; + + months.forEach((eachMonth) => { + if (monthAsString.includes(eachMonth.name)) { + monthOfMonth += eachMonth.number; + } + }); + + // Completing start of month + let completeMonthStart = `${yearOfMonth}-${monthOfMonth}-01`; + + // Setting up start + let startOfMonth = moment(completeMonthStart) + .startOf("month") + .format("MM-DD-YYYY"); + setStart(startOfMonth); + + // Setting up end + let endOfMonth = moment(startOfMonth).endOf("month").format("MM-DD-YYYY"); + setEnd(endOfMonth); + }; + + /* Calculate start and end of year */ + const startEndOfYear = () => { + // Transforming year value into string + let yearAsString = defaultYear.toString() + + // Calculating start and end + let startOfYear = moment(`01-01-${yearAsString}`).format("MM-DD-YYYY") + let endOfYear = moment(`12-31-${yearAsString}`).format("MM-DD-YYYY") + + // Setting up start and end + setStart(startOfYear) + setEnd(endOfYear) + }; + + /* + Variables with the current start and end values + of day, week month, and year + */ + let currentDay = moment().format("MM-DD-YYYY"); + let currentWeekStart = moment().isoWeekday(1).format("MM-DD-YYYY"); + let currentWeekEnd = moment().isoWeekday(7).format("MM-DD-YYYY"); + let currentMonthStart = moment().startOf("month").format("MM-DD-YYYY"); + let currentMonthEnd = moment().endOf("month").format("MM-DD-YYYY"); + let currentYearStart = moment().startOf("year").format("MM-DD-YYYY"); + let currentYearEnd = moment().endOf("year").format("MM-DD-YYYY"); + + /* + UseEffect for the first render of + the page + */ + useEffect(() => { + setPeriod("Day"); + setStart(currentDay); + setEnd(currentDay); + }, []); + + /* + Use effects that get triggered + when the period value changes + */ + useEffect(() => { + if (period === "Day") { + setStart(currentDay); + setEnd(currentDay); + } else if (period === "Week") { + setStart(currentWeekStart); + setEnd(currentWeekEnd); + } else if (period === "Month") { + setStart(currentMonthStart); + setEnd(currentMonthEnd); + } else if (period === "Year") { + setStart(currentYearStart); + setEnd(currentYearEnd); + } else { + null; + } + }, [period]); + + /* + Use effects that get triggered + when the default values change + */ + useEffect(() => { + startEndOfDay(); + }, [defaultDay]); + + useEffect(() => { + startEndOfWeek(); + }, [defaultWeek]); + + useEffect(() => { + startEndOfMonth(); + }, [defaultMonth]); + + useEffect(() => { + startEndOfYear(); + }, [defaultYear]); + + /* + Function that filters shifts based on the + start and end of the selected date + */ + const filterShifts = () => { + + // Array for filtered shifts + let filteredShifts = []; + + // Keeping shifts that exist within the selected dates + shiftProps?.forEach((shift) => { + let shiftStart = moment(shift.starting_at).format("MM-DD-YYYY"); + let shiftEnd = moment(shift.ending_at).format("MM-DD-YYYY"); + + let shiftYear = shiftStart.slice(-4); + let startAndEndYear = start.slice(-4); + + if (shiftYear === startAndEndYear) { + if ( + shiftStart >= start && + shiftStart <= end && + shiftEnd >= start && + shiftEnd <= end + ) { + filteredShifts.push(shift); + } + } + }); + + // Returning filtered shifts + return filteredShifts; + }; + + // Capturing filtered shifts + let shiftsFiltered = filterShifts(); + + // Setting up main data source for chart and table + let HoursData = HoursDataGenerator(shiftsFiltered) + + // Data for pie chart ------------------------------------------------------------------------------------- + + // Colors + const purple = "#5c00b8"; + const lightTeal = "#00ebeb"; + const darkTeal = "#009e9e"; + const lightPink = "#eb00eb"; + const darkPink = "#b200b2"; + + // Preparing data to be passed to the chart component + const hoursData = { + labels: HoursData.map((data) => data.description), + datasets: [{ + label: "Hours", + data: HoursData.map((data) => data.qty), + backgroundColor: [ + purple, darkPink, lightPink, lightTeal, darkTeal + ], + }] + } + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    +
    +
    + {/* Time period Announcer */} +
    +

    Start: {start}

    +

    End: {end}

    +
    + + {/* Dropdown */} +
    +
    + +
    + + + + +
    +
    +
    + +
    + {/* DatePicker Conditional Rendering Starts */} + <> + {period === "Day" ? ( +
    +
    +

    {`${period}: ${defaultDay}`}

    +
    + + +
    + ) : period === "Week" ? ( +
    +
    +

    {`${period}: ${defaultWeek}`}

    +
    + + +
    + ) : period === "Month" ? ( +
    +
    +

    {`${period}: ${defaultMonth}`}

    +
    + + +
    + ) : period === "Year" ? ( +
    +
    +

    {`${period}: ${defaultYear}`}

    +
    + + +
    + ) : null} + + {/* DatePicker Conditional Rendering Ends */} +
    +
    +
    + +
    + {/* Left Column Starts */} +
    +
    + {/* Hours Table Starts */} +
    +

    Hours Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {HoursData.map((item, i) => { + return item.description === "Available Hours" ? ( + + + + + + ) : ( + + + + + + ) + })} + +

    Description

    Quantity

    Percentages

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* Hours Table Ends */} +
    +
    + {/* Left Column Ends */} + + {/* Right Column Starts */} +
    +
    + {/* Hours Chart Starts*/} +
    +

    Hours Chart

    + +
    + +
    +
    + {/* Hours Chart Ends*/} +
    +
    + {/* Right Column Ends */} +
    +
    + ) +} diff --git a/src/js/views/metrics/general-stats/Hours/HoursData.js b/src/js/views/metrics/general-stats/Hours/HoursData.js new file mode 100644 index 0000000..8389982 --- /dev/null +++ b/src/js/views/metrics/general-stats/Hours/HoursData.js @@ -0,0 +1,360 @@ +import moment from "moment"; + +/** + * @function + * @description Takes in list a of shifts and generates data of all the hours trends for Hours.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @param {object} props - Contains an array of all the shifts. + */ +export const HoursDataGenerator = (props) => { + + // Assigning props to variable + let shifts = props + + // 1st - Separation of shifts ---------------------------------------------------------- + + // Array for shifts with multiple clock-ins + let multipleClockIns = []; + + // Array for single clock-ins made by single workers + let singleClockInSingleWorker = []; + + // Gathering both clock-ins and clock-outs in a formatted + // way to keep at the useful data handy at all times. + shifts.forEach((shift) => { + if (shift.clockin.length > 1) { + multipleClockIns.push({ + starting_at: shift.starting_at, + ending_at: shift.ending_at, + clockin: shift.clockin, + id: shift.id, + employees: shift.employees + }); + } else if (shift.clockin.length === 1) { + shift.clockin.forEach((clockIn) => { + singleClockInSingleWorker.push({ + id: shift.id, + starting_at: shift.starting_at, + ending_at: shift.ending_at, + started_at: clockIn.started_at, + ended_at: clockIn.ended_at + }); + }); + } + }); + + // Setting up arrays for shifts with multiple + // clock-ins but different amount of workers + let multipleClockInsMultipleWorkers = []; + let multipleClockInsSingleWorker = []; + + // Separating shifts based on the number of workers present + multipleClockIns.forEach((shift) => { + if (shift.employees.length > 1) { + // Adding shifts to 'multipleClockInsMultipleWorkers' + multipleClockInsMultipleWorkers.push(shift.clockin); + } else if (shift.employees.length === 1) { + // Adding shifts to 'multipleClockInsSingleWorker' + multipleClockInsSingleWorker.push(shift.clockin); + } + }); + + // Array of multiple clock-ins with multiple workers, but organized + let MCIMWOrganized = []; + + // Adding shifts to 'MCIMWOrganized' + multipleClockInsMultipleWorkers.forEach((shift) => { + // Unifying shifts that have the same worker + let newObj = shift.reduce((obj, value) => { + let key = value.employee; + if (obj[key] == null) obj[key] = []; + + obj[key].push(value); + return obj; + }, []); + + newObj.forEach((shift) => { + MCIMWOrganized.push(shift); + }); + }); + + // Array for the polished version of 'multipleClockInsMultipleWorkers' + let MCIMWPolished = []; + + // Array for single clock-ins made by single workers + // inside shifts with multiple workers present + let singleClockinsMultipleWorkers = []; + + // Separating shifts of multiple workers based on + // how many clock-ins each worker has + MCIMWOrganized.forEach((shift) => { + if (shift.length === 1) { + shift.forEach((clockIn) => { + singleClockinsMultipleWorkers.push(clockIn); + }); + } else if (shift.length > 1) { + MCIMWPolished.push(shift); + } + }); + + // Array for polished version of 'singleClockinsMultipleWorkers' + let SCIMWPolished = []; + + // Adding shifts to 'SCIMWPolished' in a formatted + // way to keep at the useful data handy at all times. + shifts.forEach((originalShift) => { + singleClockinsMultipleWorkers.forEach((filteredShift) => { + if (originalShift.id === filteredShift.shift) { + SCIMWPolished.push({ + id: originalShift.id, + started_at: filteredShift.started_at, + ended_at: filteredShift.ended_at, + starting_at: originalShift.starting_at, + ending_at: originalShift.ending_at + }); + } + }); + }); + + // Combining all shifts with single clock-ins. These will not have break times. + let singleClockInsCombined = [...singleClockInSingleWorker, ...SCIMWPolished]; + + // Combining all shifts with multiple clock-ins. These will have break times. + let multipleClockInsCombined = [ + ...multipleClockInsSingleWorker, + ...MCIMWPolished + ]; + + // 2nd - Calculation of Hours and Minutes ---------------------------------------------- + + // Calculating scheduled hours of all single clock-in shifts --------------------------- + + let SCICScheduledHours = singleClockInsCombined.reduce( + (total, { starting_at, ending_at }) => + total + + moment.duration(moment(ending_at).diff(moment(starting_at))).asHours(), + 0 + ); + + // Total scheduled hours + let SCICScheduledHoursF = parseInt( + (Math.round(SCICScheduledHours * 4) / 4).toFixed(0), + 10 + ); + + // Calculating worked hours of all single clock-in shifts ------------------------------ + + let SCICWorkedHours = singleClockInsCombined.reduce( + (total, { started_at, ended_at }) => + total + + moment.duration(moment(ended_at).diff(moment(started_at))).asHours(), + 0 + ); + + // Total worked hours + let SCICWorkedHoursF = parseInt( + (Math.round(SCICWorkedHours * 4) / 4).toFixed(0), + 10 + ); + + // Extra worked hours + let extraWorkedHoursSingleClockIns = SCICWorkedHoursF - SCICScheduledHoursF; + + // Calculating scheduled hours of all multiple clock-in shifts ------------------------- + + // Array for scheduled minutes + let MCICScheduledMinutes = []; + + // Adding shifts to 'MCICScheduledMinutes' + multipleClockInsCombined.forEach((shift) => { + let shiftStart = moment(shift[0].started_at); + let shiftEnd = moment(shift[shift.length - 1].ended_at); + let id = shift[0].shift; + let diff = moment.duration(shiftEnd.diff(shiftStart)).asMinutes(); + + MCICScheduledMinutes.push({ + id: id, + employee: shift[0].employee, + scheduled_mins: diff + }); + }); + + // Total scheduled minutes + let TotalMCICScheduledMinutes = MCICScheduledMinutes.reduce((acc, obj) => { + return acc + obj.scheduled_mins; + }, 0); + + // Total scheduled hours + let MCICScheduledHours = + Math.floor(TotalMCICScheduledMinutes / 60) + SCICScheduledHoursF; + + // Calculating worked hours of all multiple clock-in shifts ---------------------------- + + // Array for worked minutes + let MCICWorkedMinutes = []; + + // Adding shifts to 'MCICWorkedMinutes' + multipleClockInsCombined.forEach((shift) => { + shift.forEach((clockIn) => { + let start = moment(clockIn.started_at); + let end = moment(clockIn.ended_at); + let id = clockIn.shift; + + let diff = moment.duration(end.diff(start)).asMinutes(); + + MCICWorkedMinutes.push({ + id: id, + employee: clockIn.employee, + worked_mins: diff + }); + }); + }); + + // Polished version of 'MCICWorkedMinutes' + let MCICWorkedMinutesPolished = MCICWorkedMinutes.reduce( + (result, { id, employee, worked_mins }) => { + let temp = result.find((o) => { + return o.id === id && o.employee === employee; + }); + if (!temp) { + result.push({ id, employee, worked_mins }); + } else { + temp.worked_mins += worked_mins; + } + return result; + }, + [] + ); + + // Total worked minutes + let TotalMCICWorkedMinutes = MCICWorkedMinutesPolished.reduce((acc, obj) => { + return acc + obj.worked_mins; + }, 0); + + // Total worked hours + let MCICWorkedHours = + Math.floor(TotalMCICWorkedMinutes / 60) + SCICWorkedHoursF; + + // Extra worked hours + let extraWorkedHoursMultipleClockIns = MCICWorkedHours - MCICScheduledHours; + + // Extra calculations ----------------------------------------------------------------- + + // Array for the break times + let breakTimes = []; + + // Calculating break times of every shift + MCICScheduledMinutes.forEach((scheduledShift) => { + MCICWorkedMinutesPolished.forEach((workedShift) => { + if ( + scheduledShift.id === workedShift.id && + scheduledShift.employee === workedShift.employee + ) { + let scheduled = scheduledShift.scheduled_mins; + let worked = workedShift.worked_mins; + + let diff = scheduled - worked; + + breakTimes.push({ + id: scheduledShift.id, + break_mins: diff + }); + } + }); + }); + + // Array for long breaks + let longBreaks = []; + + // Adding shifts to 'longBreaks' + breakTimes.forEach((shift) => { + if (shift.break_mins > 30) { + longBreaks.push(shift); + } + }); + + // Calculating worked hours + let workedHours = MCICWorkedHours + SCICWorkedHoursF; + + // Calculating scheduled hours + let scheduledHours = MCICScheduledHours + SCICScheduledHoursF; + + // Calculating extra worked hours + let extraWorkedHours = + extraWorkedHoursSingleClockIns + extraWorkedHoursMultipleClockIns; + + // Setting up conditional rendering of extra worked hours + let qtyOfExtraWorkedHours = () => { + if (extraWorkedHours > 0) { + return extraWorkedHours; + } else { + return 0; + } + }; + + // 3rd - Setting up objects ----------------------------------------------------------- + + // THIS IS A PLACEHOLDER, this number should be + // the total hours available of all employees + let availableHours = 300; + + // Creating object for scheduled hours + let scheduledHoursObj = { + description: "Scheduled Hours", + qty: scheduledHours + }; + + // Creating object for worked hours + let workedHoursObj = { + description: "Worked Hours", + qty: workedHours + }; + + // Creating object for extra worked hours + let extraWorkedHoursObj = { + description: "Extra Worked Hours", + qty: qtyOfExtraWorkedHours() + }; + + // Creating object for long breaks + let longBreaksObj = { + description: "Long Breaks", + qty: `${longBreaks.length}` + }; + + // Generate semi-final list + let semiFinalList = []; + + // Adding objects to semi-final list + semiFinalList.push(scheduledHoursObj); + semiFinalList.push(workedHoursObj); + semiFinalList.push(extraWorkedHoursObj); + semiFinalList.push(longBreaksObj); + + // Generating final array with percentages as new properties + let finalList = semiFinalList.map(({ description, qty }) => ({ + description, + qty, + pct: ((qty * 100) / availableHours).toFixed(0) + })); + + // Generating object of available hours + let availableHoursObj = { + description: "Available Hours", + qty: availableHours, + pct: "100" + }; + + // Adding object of available hours to final list + finalList.push(availableHoursObj); + + // Adding IDs to each object in the array + finalList.forEach((item, i) => { + item.id = i + 1; + }); + + // Returning the final array + return finalList; +}; \ No newline at end of file diff --git a/src/js/views/metrics/general-stats/JobSeekers/JobSeekers.js b/src/js/views/metrics/general-stats/JobSeekers/JobSeekers.js new file mode 100644 index 0000000..79cb33a --- /dev/null +++ b/src/js/views/metrics/general-stats/JobSeekers/JobSeekers.js @@ -0,0 +1,222 @@ +import React, { useEffect, useState } from "react"; +import { PieChart, BarChart } from '../../charts'; +import { JobSeekersDataGenerator, NewJobSeekersDataGenerator } from "./JobSeekersData"; + +/** + * @function + * @description Creates a page with 2 graphs and 2 charts showing trends on active, inactive, and new job seekers. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires PieChart + * @requires BarChart + * @requires NewJobSeekersDataGenerator + * @requires JobSeekersDataGenerator + * @param {object} props - Contains an array of all the shifts, and also an array of all the workers. + */ +export const JobSeekers = (props) => { + + /* Variables --------------------------------------------------- */ + + /* Use state to hold list of workers and list of shifts */ + const [workersList, setWorkersList] = useState([]) + const [shifsList, setShiftsList] = useState([]) + + /* Receiving the props that contain the lists we need */ + const handleProps = async () => { + + /* Catching the props when they arrive */ + let propsObj = await props + + /* Checking length of lists before save them */ + if (propsObj.workers.length > 0) { + // Saving list of workers + setWorkersList(propsObj.workers) + // Saving list of shifts + setShiftsList(propsObj.shifts) + } else { + // Handling error with a message + console.log("Waiting for props to arrive") + } + } + + /* Triggering handleProps when props change/arrive */ + useEffect(() => { + handleProps() + }, [props]) + + if (workersList.length > 0) { + + // Setting up main data sources + let JobSeekersData = JobSeekersDataGenerator(shifsList, workersList) + let NewJobSeekersData = NewJobSeekersDataGenerator(workersList) + + // Data for pie chart ------------------------------------------------------------------------------------- + + // Colors + const purple = "#5c00b8"; + const lightPink = "#eb00eb"; + const darkTeal = "#009e9e"; + const green = "#06ff05"; + + // Taking out the "Totals" from the chart view + let pieData = JobSeekersData.filter((item) => { return item.description !== "Total Job Seekers" }) // Taking out the "Totals" from the chart view + + // Preparing data to be passed to the chart component + const jobSeekersData = { + labels: pieData.map((data) => data.description), + datasets: [{ + label: "Job Seekers", + data: pieData.map((data) => data.qty), + backgroundColor: [ + purple, lightPink + ], + }] + } + + // Data for bar chart ------------------------------------------------------------------------------------- + + // Preparing data to be passed to the chart component + const newJobSeekersData = { + labels: NewJobSeekersData.map((data) => data.description), + datasets: [{ + label: "New Job Seekers", + data: NewJobSeekersData.map((data) => data.qty), + backgroundColor: [ + darkTeal, green + ], + }] + } + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    +
    +
    + +
    +
    + +
    + {/* Left Column Starts */} +
    +
    + + {/* Job Seekers Table Starts */} +
    +

    Job Seekers Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {JobSeekersData.map((item, i) => { + return item.description === "Total Job Seekers" ? ( + + + + + + ) : + ( + + + + + + ) + })} + +

    Description

    Quantity

    Percentages

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* Job Seekers Table Ends */} +
    + +
    + {/* New Job Seekers Table Starts */} +
    +

    New Job Seekers Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {NewJobSeekersData.map((item, i) => { + return item.description === "Total Job Seekers" ? ( + + + + + + ) : + ( + + + + + + ) + })} + +

    Description

    Quantity

    Percentages

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* New Job Seekers Table Ends */} +
    +
    + {/* Left Column Ends */} + + {/* Right Column Starts */} +
    +
    + {/* Job Seekers Chart Starts*/} +
    +

    Job Seekers Chart

    + +
    + +
    +
    + {/* Job Seekers Chart Ends*/} +
    + +
    + {/* New Job Seekers Chart Starts*/} +
    +

    New Job Seekers Chart

    + +
    + +
    +
    + {/* New Job Seekers Chart Ends*/} +
    +
    + {/* Right Column Ends */} +
    +
    + ) + } else { + return ( +

    Loading

    + ) + } +} \ No newline at end of file diff --git a/src/js/views/metrics/general-stats/JobSeekers/JobSeekersData.js b/src/js/views/metrics/general-stats/JobSeekers/JobSeekersData.js new file mode 100644 index 0000000..14039b9 --- /dev/null +++ b/src/js/views/metrics/general-stats/JobSeekers/JobSeekersData.js @@ -0,0 +1,175 @@ +import moment from "moment"; + +// Today +const now = moment().format("YYYY-MM-DD") + +// Today, four weeks in the past +const fourWeeksBack = moment().subtract(4, 'weeks').format("YYYY-MM-DD") + +// Job Seekers Data ------------------------------------------------------------------ + +/** + * @function + * @description Takes in list a of shifts and job seekers and generates data of inactive/active job seekers for JobSeekers.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @param {object} props - Contains an array of all the shifts, and also an array of all the workers. + */ +export const JobSeekersDataGenerator = (shiftsProp, workersProp) => { + + // Assigning props to variables + let shifts = shiftsProp + let workers = workersProp + + // Array for all clock-ins + let clockInsList = [] + + // Gathering the clock-ins of all the shifts + shifts.forEach((shift) => { + shift.clockin.forEach((clockIn) => { + // Keeping all clockins array with at + // least one object inside + if (shift.clockin.length > 0) { + clockInsList.push(clockIn); + } + }) + }) + + // Array for all recent clock-ins + let recentClockIns = [] + + // Filtering out clock-ins that happened longer than 4 weeks ago + clockInsList.forEach((clockIn) => { + let clockInStart = moment(clockIn.started_at).format("YYYY-MM-DD"); + + if (clockInStart > fourWeeksBack && clockInStart < now) { + recentClockIns.push(clockIn) + } + }) + + // Array for worker ids + let workerIDs = [] + + // Gethering all worker ids from recent clock-ins + recentClockIns.forEach((clockIn) => { + workerIDs.push(clockIn.employee) + }) + + // Filtering out repeated worker ids + let filteredWorkerIDs = [...new Set(workerIDs)]; + + // Calculating total, active, and inactive workers + let totalWorkers = workers.length + let totalActiveWorkers = filteredWorkerIDs.length + let totalInactiveWorkers = totalWorkers - totalActiveWorkers + + // Setting up objects for the semi-final array + let activeWorkers = { + id: 1, + description: "Active Job Seekers", + qty: totalActiveWorkers + } + let inactiveWorkers = { + id: 2, + description: "Inactive Job Seekers", + qty: totalInactiveWorkers + } + + // Creating the semi-final array + let semiFinalList = [] + + // Adding objects to the semi-final array + semiFinalList.push(activeWorkers) + semiFinalList.push(inactiveWorkers) + + // Generating final array with percentages as new properties + let finalList = semiFinalList.map(({ id, description, qty }) => ({ + id, + description, + qty, + pct: ((qty * 100) / totalWorkers).toFixed(0) + })); + + // Generating the object of total workers + let totalJobSeekers = { + id: 3, + description: "Total Job Seekers", + qty: totalWorkers, + pct: "100" + } + + // Adding the object of total workers to the final array + finalList.push(totalJobSeekers) + + // Returning the final array + return finalList +} + +// New Job Seekers Data ------------------------------------------------------------------ + +/** + * @function + * @description Takes in list a of job seekers and generates data of new job seekers for JobSeekers.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @param {object} props - Contains an array of all the shifts, and also an array of all the workers. + */ +export const NewJobSeekersDataGenerator = (props) => { + + // Assigning props to variable + // Here we only need the array of workers + let workers = props + + // Array for new workers + let newWorkersList = [] + + // Adding workers to 'newWorkersList' based on their creation date + workers.forEach((worker) => { + let creation_date = moment(worker.created_at).format("YYYY-MM-DD"); + + if (creation_date > fourWeeksBack && creation_date < now) { + newWorkersList.push(worker) + } + }) + + // Setting up some variables for the objects + let totalWorkers = workers.length + let totalNewWorkers = newWorkersList.length + + // Setting up objects for the semi-final array + let newWorkers = { + id: 0, + description: "New Job Seekers", + qty: totalNewWorkers + } + + // Creating the semi-final array + let semiFinalList = [] + + // Adding objects to the semi-final array + semiFinalList.push(newWorkers) + + // Generating final array with percentages as new properties + let finalList = semiFinalList.map(({ id, description, qty }) => ({ + id, + description, + qty, + pct: ((qty * 100) / totalWorkers).toFixed(0) + })); + + // Generating the object of total workers + let totalJobSeekers = { + id: 1, + description: "Total Job Seekers", + qty: totalWorkers, + pct: "100" + } + + // Adding the object of total workers to the final array + finalList.push(totalJobSeekers) + + // Returning the final array + return finalList +} \ No newline at end of file diff --git a/src/js/views/metrics/general-stats/Shifts/Shifts.js b/src/js/views/metrics/general-stats/Shifts/Shifts.js new file mode 100644 index 0000000..23a21ad --- /dev/null +++ b/src/js/views/metrics/general-stats/Shifts/Shifts.js @@ -0,0 +1,526 @@ +import React, { useState, useEffect } from "react"; +import { BarChart } from "../../charts"; +import { ShiftsDataGenerator } from "./ShiftsData"; +import weekends from "react-multi-date-picker/plugins/highlight_weekends" +import DatePicker from "react-multi-date-picker"; +import moment from "moment"; + +/** + * @function + * @description Creates a page with a table and a graph of all the shift statuses. + * @since 10.28.22 by Paola Sanchez + * @author Paola Sanchez + * @requires BarChart + * @requires ShiftsDataGenerator + * @param {object} props - Contains an array of all the shifts. + */ +export const Shifts = (props) => { + + /* Variables --------------------------------------------------- */ + + /* Putting props into variable */ + let shiftProps = props.shifts; + + /* Start of given time period */ + const [start, setStart] = useState(moment().format("MM-DD-YYYY")); + + /* End of given time period */ + const [end, setEnd] = useState(moment().format("MM-DD-YYYY")); + + /* Variables to store selected time period */ + const [period, setPeriod] = useState(); + + /* Variables to store selected dates */ + const [day, setDay] = useState(new Date()); + const [week, setWeek] = useState([ + new Date(), + new Date() + ]) + const [month, setMonth] = useState(new Date()); + const [year, setYear] = useState(new Date()); + + /* Variables to store date defaults*/ + const [defaultDay, setDefaultDay] = useState(new Date().toLocaleDateString()); + const [defaultWeek, setDefaultWeek] = useState( + new Date().toLocaleDateString() + ); + + const [defaultMonth, setDefaultMonth] = useState( + new Date().toLocaleDateString("en-us", { month: "long", year: "numeric" }) + ); + + const [defaultYear, setDefaultYear] = useState(new Date().getFullYear()); + + /* Handlers --------------------------------------------------- */ + + /* Day Handler */ + const handleDay = (day) => { + setDay(day); + setDefaultDay(day); + }; + + /* Week Handler */ + const handleWeek = (week) => { + setWeek(week); + setDefaultWeek(week); + }; + + /* Month Handler */ + const handleMonth = (month) => { + setMonth(month); + setDefaultMonth(month); + }; + + /* Year Handler */ + const handleYear = (year) => { + setYear(year); + setDefaultYear(year); + }; + + /* Calculate start and end of day */ + const startEndOfDay = () => { + // Transforming day value into string + let dayAsString = defaultDay.toString(); + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const dayFormatted = dayAsString.split(search).join(replaceWith); + + // Setting start and end + setStart(dayFormatted); + setEnd(dayFormatted); + }; + + /* Calculate start and end of week */ + const startEndOfWeek = () => { + // Transforming week value into string + let weekAsString = defaultWeek.toString(); + + let weeks = weekAsString.split(","); + + let endOfWeekPlaceholder = moment().isoWeekday(7).format("MM-DD-YYYY"); + + if (weeks.length === 1) { + weeks.push(endOfWeekPlaceholder); + } + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const weekStart = weeks[0].split(search).join(replaceWith); + const weekEnd = weeks[1].split(search).join(replaceWith); + + let weekStartF = moment(weekEnd).isoWeekday(1).format("MM-DD-YYYY"); + let weekEndF = moment(weekEnd).isoWeekday(7).format("MM-DD-YYYY"); + + // Setting start and end + setStart(weekStartF); + setEnd(weekEndF); + }; + + /* Calculate start and end of month */ + const startEndOfMonth = () => { + // Transforming month value into string + let monthAsString = defaultMonth.toString(); + + // Listing all months by number + const months = [ + { name: "January", number: "01" }, + { name: "February", number: "02" }, + { name: "March", number: "03" }, + { name: "April", number: "04" }, + { name: "May", number: "05" }, + { name: "June", number: "06" }, + { name: "July", number: "07" }, + { name: "August", number: "08" }, + { name: "September", number: "09" }, + { name: "October", number: "10" }, + { name: "November", number: "11" }, + { name: "December", number: "12" } + ]; + + // Determining year (number) of month value + let yearOfMonth = monthAsString.slice(-4); + + // Determining month (number) of month value + let monthOfMonth = ""; + + months.forEach((eachMonth) => { + if (monthAsString.includes(eachMonth.name)) { + monthOfMonth += eachMonth.number; + } + }); + + // Completing start of month + let completeMonthStart = `${yearOfMonth}-${monthOfMonth}-01`; + + // Setting up start + let startOfMonth = moment(completeMonthStart) + .startOf("month") + .format("MM-DD-YYYY"); + setStart(startOfMonth); + + // Setting up end + let endOfMonth = moment(startOfMonth).endOf("month").format("MM-DD-YYYY"); + setEnd(endOfMonth); + }; + + /* Calculate start and end of year */ + const startEndOfYear = () => { + // Transforming year value into string + let yearAsString = defaultYear.toString() + + // Calculating start and end + let startOfYear = moment(`01-01-${yearAsString}`).format("MM-DD-YYYY") + let endOfYear = moment(`12-31-${yearAsString}`).format("MM-DD-YYYY") + + // Setting up start and end + setStart(startOfYear) + setEnd(endOfYear) + }; + + /* + Variables with the current start and end values + of day, week month, and year + */ + let currentDay = moment().format("MM-DD-YYYY"); + let currentWeekStart = moment().isoWeekday(1).format("MM-DD-YYYY"); + let currentWeekEnd = moment().isoWeekday(7).format("MM-DD-YYYY"); + let currentMonthStart = moment().startOf("month").format("MM-DD-YYYY"); + let currentMonthEnd = moment().endOf("month").format("MM-DD-YYYY"); + let currentYearStart = moment().startOf("year").format("MM-DD-YYYY"); + let currentYearEnd = moment().endOf("year").format("MM-DD-YYYY"); + + /* + UseEffect for the first render of + the page + */ + useEffect(() => { + setPeriod("Day"); + setStart(currentDay); + setEnd(currentDay); + }, []); + + /* + Use effects that get triggered + when the period value changes + */ + useEffect(() => { + if (period === "Day") { + setStart(currentDay); + setEnd(currentDay); + } else if (period === "Week") { + setStart(currentWeekStart); + setEnd(currentWeekEnd); + } else if (period === "Month") { + setStart(currentMonthStart); + setEnd(currentMonthEnd); + } else if (period === "Year") { + setStart(currentYearStart); + setEnd(currentYearEnd); + } else { + null; + } + }, [period]); + + /* + Use effects that get triggered + when the default values change + */ + useEffect(() => { + startEndOfDay(); + }, [defaultDay]); + + useEffect(() => { + startEndOfWeek(); + }, [defaultWeek]); + + useEffect(() => { + startEndOfMonth(); + }, [defaultMonth]); + + useEffect(() => { + startEndOfYear(); + }, [defaultYear]); + + /* + Function that filters shifts based on the + start and end of the selected date + */ + const filterShifts = () => { + + // Array for filtered shifts + let filteredShifts = []; + + // Keeping shifts that exist within the selected dates + shiftProps?.forEach((shift) => { + let shiftStart = moment(shift.starting_at).format("MM-DD-YYYY"); + let shiftEnd = moment(shift.ending_at).format("MM-DD-YYYY"); + + let shiftYear = shiftStart.slice(-4); + let startAndEndYear = start.slice(-4); + + if (shiftYear === startAndEndYear) { + if ( + shiftStart >= start && + shiftStart <= end && + shiftEnd >= start && + shiftEnd <= end + ) { + filteredShifts.push(shift); + } + } + }); + + // Returning filtered shifts + return filteredShifts; + }; + + // Capturing filtered shifts + let shiftsFiltered = filterShifts(); + + // Setting up main data source for chart and table + let ShiftsData = ShiftsDataGenerator(shiftsFiltered); + + // Data for bar chart ------------------------------------------------------------------------------------- + + // Colors + const purple = "#5c00b8"; + const lightTeal = "#00ebeb"; + const darkTeal = "#009e9e"; + const lightPink = "#eb00eb"; + const darkPink = "#b200b2"; + + // Taking out the "Totals" from the chart view + let barData = ShiftsData.filter((item) => { + return item.description !== "Total Shifts Posted "; + }); // Taking out the "Totals" from the chart view + + // Preparing data to be passed to the chart component + const shiftsData = { + labels: barData.map((data) => data.description), + datasets: [ + { + label: "Shifts", + data: barData.map((data) => data.qty), + backgroundColor: [purple, darkPink, lightPink, lightTeal, darkTeal] + } + ] + }; + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    +
    +
    + {/* Time period Announcer */} +
    +

    Start: {start}

    +

    End: {end}

    +
    + + {/* Dropdown */} +
    +
    + +
    + + + + +
    +
    +
    + +
    + {/* DatePicker Conditional Rendering Starts */} + <> + {period === "Day" ? ( +
    +
    +

    {`${period}: ${defaultDay}`}

    +
    + + +
    + ) : period === "Week" ? ( +
    +
    +

    {`${period}: ${defaultWeek}`}

    +
    + + +
    + ) : period === "Month" ? ( +
    +
    +

    {`${period}: ${defaultMonth}`}

    +
    + + +
    + ) : period === "Year" ? ( +
    +
    +

    {`${period}: ${defaultYear}`}

    +
    + + +
    + ) : null} + + {/* DatePicker Conditional Rendering Ends */} +
    +
    +
    + +
    + {/* Left Column Starts */} +
    +
    + {/* Shifts Table Starts */} +
    +

    Shifts Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {ShiftsData.map((item, i) => { + return item.description === "Total Shifts Posted" ? ( + + + + + + ) : ( + + + + + + ); + })} + +
    +

    Description

    +
    +

    Quantity

    +
    +

    Percentages

    +
    +

    {item.description}

    +
    +

    {item.qty}

    +
    +

    {`${item.pct}%`}

    +
    +

    {item.description}

    +
    +

    {item.qty}

    +
    +

    {`${item.pct}%`}

    +
    +
    + {/* Shifts Table Ends */} +
    +
    + {/* Left Column Ends */} + + {/* Right Column Starts */} +
    +
    + {/* Shifts Chart Starts*/} +
    +

    Shifts Chart

    + +
    + +
    +
    + {/* Shifts Chart Ends*/} +
    +
    + {/* Right Column Ends */} +
    +
    + ); +}; \ No newline at end of file diff --git a/src/js/views/metrics/general-stats/Shifts/ShiftsData.js b/src/js/views/metrics/general-stats/Shifts/ShiftsData.js new file mode 100644 index 0000000..a5e6c5d --- /dev/null +++ b/src/js/views/metrics/general-stats/Shifts/ShiftsData.js @@ -0,0 +1,96 @@ +/** + * @function + * @description Takes in list a of shifts and generates data of shift statuses for Shifts.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @param {object} props - Contains a list of all shifts. + */ +export const ShiftsDataGenerator = (props) => { + + // Assigning props to variable + let shifts = props + + // First array + let shiftsList = []; + + // Gathering all the existing shifts + shifts.forEach((shift) => { + shiftsList.push({ + status: shift.status, + clockin: shift.clockin, + employees: shift.employees + }); + }); + + // Setting up counters + let open = 0 + let filled = 0 + let completed = 0 + let rejected = 0 + let total = shiftsList.length + + // Adding values to each counter based on certain shift conditions + shiftsList.forEach((item) => { + if (item.status === "EXPIRED" && item.clockin.length === 0 && item.employees.length === 0) { + rejected++ + } else if (item.status === "FILLED") { + filled++ + } else if (item.status === "COMPLETED") { + completed++ + } else if (item.status === "OPEN") { + open++ + } + }) + + // Creating shift objects + let openShifts = { + description: "Open Shifts", + qty: open + } + let filledShifts = { + description: "Filled Shifts", + qty: filled + } + let workedShifts = { + description: "Worked Shifts", + qty: completed + } + let rejectedShifts = { + description: "Rejected Shifts", + qty: rejected + } + + // Setting up base array for all shift objects + let cleanedArray = [] + + // Pushing shift objects to base array + cleanedArray.push(openShifts) + cleanedArray.push(filledShifts) + cleanedArray.push(workedShifts) + cleanedArray.push(rejectedShifts) + + // Generating final array with percentages as new properties + let percentagesArray = cleanedArray.map(({ description, qty }) => ({ + description, + qty, + pct: ((qty * 100) / total).toFixed(0) + })); + + // Generating the object of total shifts + let totalShifs = { + description: "Total Shifts Posted", + qty: total, + pct: "100" + } + + // Adding the object of total shifts to the final array + percentagesArray.push(totalShifs) + + // Adding id's to each object in the final array + percentagesArray.forEach((item, i) => { + item.id = i + 1; + }); + + // Returning the final array + return percentagesArray +}; \ No newline at end of file diff --git a/src/js/views/metrics/metrics.js b/src/js/views/metrics/metrics.js new file mode 100644 index 0000000..4f5ba3a --- /dev/null +++ b/src/js/views/metrics/metrics.js @@ -0,0 +1,185 @@ +import React from "react"; +import Flux from "@4geeksacademy/react-flux-dash"; +import { Session } from "bc-react-session"; + +import { Queue } from "./queue/Queue"; +import { Punctuality } from "./punctuality/Punctuality"; +import { Ratings } from "./ratings/Ratings"; +import { GeneralStats } from "./general-stats/GeneralStats"; + +import { store, search } from "../../actions"; + +// import {WorkersData} from "./dummy-data/WorkersData"; +// import {ShiftsData} from "./dummy-data/ShiftsData"; + +/** + * @description Creates the view for Metrics page, which renders 4 tabs with different components being called inside each one. + * @since 09.28.22 by Paola Sanchez + * @author Paola Sanchez + * @requires Punctuality + * @requires Ratings + * @requires GeneralStats + * @requires Queue + * @requires store + * @requires search + * @requires Session + * @requries Flux + */ +export class Metrics extends Flux.DashView { + + constructor() { + super(); + this.state = { + // Queue Data ---------------------------------------------------------------------------------------------------------------------------- + + // Variables for the Workers' List + employees: [], // This will hold all the shifts. + DocStatus: "", //This is needed to check the verification status of employees. + empStatus: "unverified", //This is needed to filter out unverified employees. + + // Variables for the Shifts' List + allShifts: [], // This will hold all the shifts. + session: Session.get(), // This is needed to crate a user/employer session. + calendarLoading: true, // This is needed to fill up the list of shifts, not sure how. + }; + + // This updates the state values + this.handleStatusChange = this.handleStatusChange.bind(this); + } + + // Generating the list of shifts and the list of employees --------------------------------------------------------------------------------------- + + componentDidMount() { + // Processes for the Shifts' List (Not sure how they work) + const shifts = store.getState("shifts"); + + this.subscribe(store, "shifts", (_shifts) => { + this.setState({ allShifts: _shifts, calendarLoading: false }); + }); + + // Processes for the Workers' List (Not sure how they work) + this.filter(); + + this.subscribe(store, "employees", (employees) => { + if (Array.isArray(employees) && employees.length !== 0) + this.setState({ employees }); + }); + + this.handleStatusChange + } + + // Processes for the Workers' List (Not sure how they work) + componentWillUnmount() { + this.handleStatusChange + } + + handleStatusChange() { + this.setState({ DocStatus: props.catalog.employee.employment_verification_status }); + } + + filter(url) { + let queries = window.location.search; + + if (queries) queries = "&" + queries.substring(1); + + if (url && url.length > 50) { + const page = url.split("employees")[1]; + + if (page) { + search(`employees`, `${page + queries}`).then((data) => { + this.setState({ + employees: data.results, + }); + }); + + } else null; + + } else { + search(`employees`, `?envelope=true&limit=50${queries}`).then((data) => { + this.setState({ + employees: data.results, + }); + }); + } + } + + // Render --------------------------------------------------------------------------------------------------------------------------------------- + + render() { + // List of workers with verified documents + let verifiedEmpList = this.state.employees.filter((employees) => employees.employment_verification_status === "APPROVED") + + // List of all shifts + let listOfShifts = this.state.allShifts; + + // Temporal dummy data + // let dummyShifts = ShiftsData + // let dummyWorkers = WorkersData + + // --------------------------------------------- + // Filtering expired shifts + // let listOfShifts = + // (Array.isArray(shifts) && + // shifts.length > 0 && + // shifts.filter( + // (e) => e.status !== "EXPIRED" + // )) || + // []; + // --------------------------------------------- + + // Return ----------------------------------------------------------------------------------------------------------------------------------- + + return ( +
    + {/* Title of the Page*/} +
    +

    Metrics

    +
    + +
    + {/* Tabs Controller Starts */} + + {/* Tabs Controller Ends */} + + {/* Tabs Content Starts */} + + {/* Tabs Content Ends */} +
    +
    + ); + } +} \ No newline at end of file diff --git a/src/js/views/metrics/punctuality/Punctuality.js b/src/js/views/metrics/punctuality/Punctuality.js new file mode 100644 index 0000000..fb2518d --- /dev/null +++ b/src/js/views/metrics/punctuality/Punctuality.js @@ -0,0 +1,580 @@ +import React, { useState, useEffect } from "react"; +import { PieChart } from '../charts'; +import { ClockInsDataGenerator, ClockOutsDataGenerator } from "./PunctualityData"; +import weekends from "react-multi-date-picker/plugins/highlight_weekends" +import DatePicker from "react-multi-date-picker"; +import moment from "moment"; + +/** + * @function + * @description Creates a page with 2 tables and 2 graphs of the clock-in and clock-out trends. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires PieChart + * @requires ClockInsDataGenerator + * @requires ClockOutsDataGenerator + * @param {object} props - Contains an array of all the shifts. + */ +export const Punctuality = (props) => { + + + /* Variables --------------------------------------------------- */ + + /* Putting props into variable */ + let shiftProps = props.shifts; + + /* Start of given time period */ + const [start, setStart] = useState(moment().format("MM-DD-YYYY")); + + /* End of given time period */ + const [end, setEnd] = useState(moment().format("MM-DD-YYYY")); + + /* Variables to store selected time period */ + const [period, setPeriod] = useState(); + + /* Variables to store selected dates */ + const [day, setDay] = useState(new Date()); + const [week, setWeek] = useState([ + new Date(), + new Date() + ]) + const [month, setMonth] = useState(new Date()); + const [year, setYear] = useState(new Date()); + + /* Variables to store date defaults*/ + const [defaultDay, setDefaultDay] = useState(new Date().toLocaleDateString()); + const [defaultWeek, setDefaultWeek] = useState( + new Date().toLocaleDateString() + ); + + const [defaultMonth, setDefaultMonth] = useState( + new Date().toLocaleDateString("en-us", { month: "long", year: "numeric" }) + ); + + const [defaultYear, setDefaultYear] = useState(new Date().getFullYear()); + + /* Handlers --------------------------------------------------- */ + + /* Day Handler */ + const handleDay = (day) => { + setDay(day); + setDefaultDay(day); + }; + + /* Week Handler */ + const handleWeek = (week) => { + setWeek(week); + setDefaultWeek(week); + }; + + /* Month Handler */ + const handleMonth = (month) => { + setMonth(month); + setDefaultMonth(month); + }; + + /* Year Handler */ + const handleYear = (year) => { + setYear(year); + setDefaultYear(year); + }; + + /* Calculate start and end of day */ + const startEndOfDay = () => { + // Transforming day value into string + let dayAsString = defaultDay.toString(); + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const dayFormatted = dayAsString.split(search).join(replaceWith); + + // Setting start and end + setStart(dayFormatted); + setEnd(dayFormatted); + }; + + /* Calculate start and end of week */ + const startEndOfWeek = () => { + // Transforming week value into string + let weekAsString = defaultWeek.toString(); + + let weeks = weekAsString.split(","); + + let endOfWeekPlaceholder = moment().isoWeekday(7).format("MM-DD-YYYY"); + + if (weeks.length === 1) { + weeks.push(endOfWeekPlaceholder); + } + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const weekStart = weeks[0].split(search).join(replaceWith); + const weekEnd = weeks[1].split(search).join(replaceWith); + + let weekStartF = moment(weekEnd).isoWeekday(1).format("MM-DD-YYYY"); + let weekEndF = moment(weekEnd).isoWeekday(7).format("MM-DD-YYYY"); + + // Setting start and end + setStart(weekStartF); + setEnd(weekEndF); + }; + + /* Calculate start and end of month */ + const startEndOfMonth = () => { + // Transforming month value into string + let monthAsString = defaultMonth.toString(); + + // Listing all months by number + const months = [ + { name: "January", number: "01" }, + { name: "February", number: "02" }, + { name: "March", number: "03" }, + { name: "April", number: "04" }, + { name: "May", number: "05" }, + { name: "June", number: "06" }, + { name: "July", number: "07" }, + { name: "August", number: "08" }, + { name: "September", number: "09" }, + { name: "October", number: "10" }, + { name: "November", number: "11" }, + { name: "December", number: "12" } + ]; + + // Determining year (number) of month value + let yearOfMonth = monthAsString.slice(-4); + + // Determining month (number) of month value + let monthOfMonth = ""; + + months.forEach((eachMonth) => { + if (monthAsString.includes(eachMonth.name)) { + monthOfMonth += eachMonth.number; + } + }); + + // Completing start of month + let completeMonthStart = `${yearOfMonth}-${monthOfMonth}-01`; + + // Setting up start + let startOfMonth = moment(completeMonthStart) + .startOf("month") + .format("MM-DD-YYYY"); + setStart(startOfMonth); + + // Setting up end + let endOfMonth = moment(startOfMonth).endOf("month").format("MM-DD-YYYY"); + setEnd(endOfMonth); + }; + + /* Calculate start and end of year */ + const startEndOfYear = () => { + // Transforming year value into string + let yearAsString = defaultYear.toString() + + // Calculating start and end + let startOfYear = moment(`01-01-${yearAsString}`).format("MM-DD-YYYY") + let endOfYear = moment(`12-31-${yearAsString}`).format("MM-DD-YYYY") + + // Setting up start and end + setStart(startOfYear) + setEnd(endOfYear) + }; + + /* + Variables with the current start and end values + of day, week month, and year + */ + let currentDay = moment().format("MM-DD-YYYY"); + let currentWeekStart = moment().isoWeekday(1).format("MM-DD-YYYY"); + let currentWeekEnd = moment().isoWeekday(7).format("MM-DD-YYYY"); + let currentMonthStart = moment().startOf("month").format("MM-DD-YYYY"); + let currentMonthEnd = moment().endOf("month").format("MM-DD-YYYY"); + let currentYearStart = moment().startOf("year").format("MM-DD-YYYY"); + let currentYearEnd = moment().endOf("year").format("MM-DD-YYYY"); + + /* + UseEffect for the first render of + the page + */ + useEffect(() => { + setPeriod("Day"); + setStart(currentDay); + setEnd(currentDay); + }, []); + + /* + Use effects that get triggered + when the period value changes + */ + useEffect(() => { + if (period === "Day") { + setStart(currentDay); + setEnd(currentDay); + } else if (period === "Week") { + setStart(currentWeekStart); + setEnd(currentWeekEnd); + } else if (period === "Month") { + setStart(currentMonthStart); + setEnd(currentMonthEnd); + } else if (period === "Year") { + setStart(currentYearStart); + setEnd(currentYearEnd); + } else { + null; + } + }, [period]); + + /* + Use effects that get triggered + when the default values change + */ + useEffect(() => { + startEndOfDay(); + }, [defaultDay]); + + useEffect(() => { + startEndOfWeek(); + }, [defaultWeek]); + + useEffect(() => { + startEndOfMonth(); + }, [defaultMonth]); + + useEffect(() => { + startEndOfYear(); + }, [defaultYear]); + + /* + Function that filters shifts based on the + start and end of the selected date + */ + const filterShifts = () => { + + // Array for filtered shifts + let filteredShifts = []; + + // Keeping shifts that exist within the selected dates + shiftProps?.forEach((shift) => { + let shiftStart = moment(shift.starting_at).format("MM-DD-YYYY"); + let shiftEnd = moment(shift.ending_at).format("MM-DD-YYYY"); + + let shiftYear = shiftStart.slice(-4); + let startAndEndYear = start.slice(-4); + + if (shiftYear === startAndEndYear) { + if ( + shiftStart >= start && + shiftStart <= end && + shiftEnd >= start && + shiftEnd <= end + ) { + filteredShifts.push(shift); + } + } + }); + + // Returning filtered shifts + return filteredShifts; + }; + + // Capturing filtered shifts + let shiftsFiltered = filterShifts(); + + // Setting up main data source for chart and table + let ClockInsData = ClockInsDataGenerator(shiftsFiltered); + let ClockOutsData = ClockOutsDataGenerator(shiftsFiltered); + + // Data for pie charts ------------------------------------------------------------------------------------- + + // Colors + const purple = "#5c00b8"; + const lightTeal = "#00ebeb"; + const darkTeal = "#009e9e"; + const green = "#06ff05"; + const lightPink = "#eb00eb"; + const darkPink = "#b200b2"; + + // Clock-Ins ------------------------------------------------------------------------------------------------ + + // Taking out the "Totals" from the chart view + let dataCI = ClockInsData.filter((item) => { + return item.description !== "Total Clock-Ins"; + }); + + // Preparing the data to be passed to the chart component + const clockInsData = { + labels: dataCI.map((data) => data.description), + datasets: [ + { + label: "Clock-Ins", + data: dataCI.map((data) => data.qty), + backgroundColor: [green, lightTeal, darkPink] + } + ] + }; + + // Clock-Outs ------------------------------------------------------------------------------------------------ + + // Taking out the "Totals" from the chart view + let dataCO = ClockOutsData.filter((item) => { + return item.description !== "Total Clock-Outs"; + }); + + // Preparing the data to be passed to the chart component + const clockOutsData = { + labels: dataCO.map((data) => data.description), + datasets: [ + { + label: "Clock-Outs", + data: dataCO.map((data) => data.qty), + backgroundColor: [purple, darkTeal, lightTeal, lightPink] + } + ] + }; + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    +
    +
    + {/* Time period Announcer */} +
    +

    Start: {start}

    +

    End: {end}

    +
    + + {/* Dropdown */} +
    +
    + +
    + + + + +
    +
    +
    + +
    + {/* DatePicker Conditional Rendering Starts */} + <> + {period === "Day" ? ( +
    +
    +

    {`${period}: ${defaultDay}`}

    +
    + + +
    + ) : period === "Week" ? ( +
    +
    +

    {`${period}: ${defaultWeek}`}

    +
    + + +
    + ) : period === "Month" ? ( +
    +
    +

    {`${period}: ${defaultMonth}`}

    +
    + + +
    + ) : period === "Year" ? ( +
    +
    +

    {`${period}: ${defaultYear}`}

    +
    + + +
    + ) : null} + + {/* DatePicker Conditional Rendering Ends */} +
    +
    +
    + +
    + {/* Left Column Starts */} +
    +
    + {/* Clock-Ins Table Starts */} +
    +

    Clock-Ins Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {ClockInsData.map((item, i) => { + return item.description === "Total Clock-Ins" ? ( + + + + + + ) : ( + + + + + + ) + })} + +

    Description

    Quantity

    Percentages

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* Clock-Ins Table Ends */} +
    + +
    + {/* Clock-Outs Table Starts */} +
    +

    Clock-Outs Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {ClockOutsData.map((item, i) => { + return item.description === "Total Clock-Outs" ? ( + + + + + + ) : ( + + + + + + ) + })} + +

    Description

    Quantity

    Percentages

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    {item.description}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* Clock-Outs Table Ends */} +
    +
    + {/* Left Column Ends */} + + {/* Right Column Starts */} +
    +
    + {/* Clock-Ins Chart Starts */} +
    +

    Clock-Ins Chart

    + +
    + +
    +
    + {/* Clock-Ins Chart Ends */} +
    + +
    + {/* Clock-Outs Chart Starts */} +
    +

    Clock-Outs Chart

    + +
    + +
    +
    + {/* Clock-Outs Chart Ends */} +
    +
    + {/* Right Column Ends */} +
    +
    + ) +} diff --git a/src/js/views/metrics/punctuality/PunctualityData.js b/src/js/views/metrics/punctuality/PunctualityData.js new file mode 100644 index 0000000..6274a93 --- /dev/null +++ b/src/js/views/metrics/punctuality/PunctualityData.js @@ -0,0 +1,232 @@ +import moment from "moment"; + +// Clock-Ins Data ------------------------------------------------------------------ + +/** + * @function + * @description Generates array of objects with clock-in trends for Punctuality.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @param {object} props - Contains an array of all the shifts. + */ +export const ClockInsDataGenerator = (props) => { + + // Assigning props to variables + let shifts = props + + // Array for the clock-ins + let clockIns = []; + + // Sorting the shifts + shifts.forEach((shift) => { + shift.clockin.forEach((clockIn) => { + // Keeping all clockins array with at + // least one object inside + if (shift.clockin.length > 0) { + // Formatting each object to keep + // both the scheduled clock-in and + // the actual "registered" clock-in. + clockIns.push({ + starting_at: shift.starting_at, + started_at: clockIn.started_at + }); + } + }); + }); + + // Setting up counters + let earlyClockins = 0; + let lateClockins = 0; + let onTimeClockins = 0; + + // Increasing counters based on clock-in times + clockIns.forEach((shift) => { + let start1 = moment(shift.starting_at); + let start2 = moment(shift.started_at); + + let startDiff = moment.duration(start2.diff(start1)).asMinutes(); + + if (startDiff >= 15) { + lateClockins++; + } else if (startDiff <= -30) { + earlyClockins++; + } else { + onTimeClockins++; + } + }); + + // Creating clock-in objects + let earlyClockinsObj = { + description: "Early Clock-Ins", + qty: earlyClockins + }; + let lateClockinsObj = { + description: "Late Clock-Ins", + qty: lateClockins + }; + let onTimeClockinsObj = { + description: "On Time Clock-Ins", + qty: onTimeClockins + }; + + // Setting up base array for all objects + let cleanedClockIns = []; + + // Pushing objects to base array + cleanedClockIns.push(earlyClockinsObj); + cleanedClockIns.push(lateClockinsObj); + cleanedClockIns.push(onTimeClockinsObj); + + // Setting up totals + let totalClockIns = clockIns.length; + + // Generating percentages as new properties + let pctClockIns = cleanedClockIns.map(({ description, qty }) => ({ + description, + qty, + pct: ((qty * 100) / totalClockIns).toFixed(0) + })); + + // Setting up object for totals + let totalClockInsObj = { + description: "Total Clock-Ins", + qty: totalClockIns, + pct: "100" + }; + + // Adding totals to the array with percentages + pctClockIns.push(totalClockInsObj); + + // Addind IDs to each object + pctClockIns.forEach((item, i) => { + item.id = i + 1; + }); + + // Returning clock-ins array + return pctClockIns; +}; + +// Clock-Outs Data ----------------------------------------------------------------- + +/** + * @function + * @description Generates array of objects with clock-out trends for Punctuality.js. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @param {object} props - Contains an array of all the shifts. + */ +export const ClockOutsDataGenerator = (props) => { + + // Assigning props to variables + let shifts = props + + // Array for the clock-outs + let clockOuts = []; + + // Sorting the shifts + shifts.forEach((shift) => { + shift.clockin.forEach((clockIn) => { + // Keeping all clockins array with at + // least one object inside + if (shift.clockin.length > 0) { + // Formatting each object to keep + // both the scheduled clock-out, the + // actual "registered" clock-out, and + // whether it closed automatically or not. + clockOuts.push({ + ending_at: shift.ending_at, + ended_at: clockIn.ended_at, + automatically_closed: clockIn.automatically_closed + }); + } + }); + }); + + // Setting up counters + let earlyClockouts = 0; + let lateClockouts = 0; + let onTimeClockouts = 0; + let forgotClockOut = 0; + + // Increasing counters based on clock-out times + clockOuts.forEach((shift) => { + let end1 = moment(shift.ending_at); + let end2 = moment(shift.ended_at); + + let endDiff = moment.duration(end2.diff(end1)).asMinutes(); + + if (endDiff >= 30) { + lateClockouts++; + } else if (endDiff <= -30) { + earlyClockouts++; + } else { + onTimeClockouts++; + } + }); + + // Increasing the "forgotClockOut" counter only + clockOuts.forEach((shift) => { + // Note: When a shif get automatically closed, it means + // that the worker forgot to clock-out. + if (shift.automatically_closed === true) { + forgotClockOut++; + } + }); + + // Creating clock-out objects + let earlyClockoutsObj = { + description: "Early Clock-Outs", + qty: earlyClockouts + }; + let lateClockoutsObj = { + description: "Late Clock-Outs", + qty: lateClockouts + }; + let onTimeClockoutsObj = { + description: "On Time Clock-Outs", + qty: onTimeClockouts + }; + let forgotClockOutObj = { + description: "Forgotten Clock-Outs", + qty: forgotClockOut + }; + + // Setting up base array for all objects + let cleanedClockOuts = []; + + // Pushing objects to base array + cleanedClockOuts.push(earlyClockoutsObj); + cleanedClockOuts.push(lateClockoutsObj); + cleanedClockOuts.push(onTimeClockoutsObj); + cleanedClockOuts.push(forgotClockOutObj); + + // Setting up totals + let totalClockOuts = clockOuts.length; + + // Generating percentages as new properties + let pctClockOuts = cleanedClockOuts.map(({ description, qty }) => ({ + description, + qty, + pct: ((qty * 100) / totalClockOuts).toFixed(0) + })); + + // Setting up object for totals + let totalClockOutsObj = { + description: "Total Clock-Outs", + qty: totalClockOuts, + pct: "100" + }; + + // Adding totals to the array with percentages + pctClockOuts.push(totalClockOutsObj); + + // Addind IDs to each object + pctClockOuts.forEach((item, i) => { + item.id = i + 1; + }); + + // Returning clock-outs array + return pctClockOuts; +}; \ No newline at end of file diff --git a/src/js/views/metrics/queue/Queue.js b/src/js/views/metrics/queue/Queue.js new file mode 100644 index 0000000..8410b25 --- /dev/null +++ b/src/js/views/metrics/queue/Queue.js @@ -0,0 +1,425 @@ +import React, { useState, useEffect } from "react"; +import { QueueData } from "./QueueData"; +import weekends from "react-multi-date-picker/plugins/highlight_weekends" +import DatePicker from "react-multi-date-picker"; +import moment from "moment"; + +/** + * @function + * @description Creates a page with a DatePicker and table of all employees with their worked/scheduled hours. + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires QueueData + * @param {object} props - Contains an array of all shifts, and an array of all workers. + */ +export const Queue = (props) => { + + /* Variables --------------------------------------------------- */ + + /* Putting props into variables */ + let shiftProps = props.shifts; + let workerProps = props.workers; + + /* Start of given time period */ + const [start, setStart] = useState(moment().format("MM-DD-YYYY")); + + /* End of given time period */ + const [end, setEnd] = useState(moment().format("MM-DD-YYYY")); + + /* Variables to store selected time period */ + const [period, setPeriod] = useState(); + + /* Variables to store selected dates */ + const [day, setDay] = useState(new Date()); + const [week, setWeek] = useState([ + new Date(), + new Date() + ]) + const [month, setMonth] = useState(new Date()); + const [year, setYear] = useState(new Date()); + + /* Variables to store date defaults*/ + const [defaultDay, setDefaultDay] = useState(new Date().toLocaleDateString()); + const [defaultWeek, setDefaultWeek] = useState( + new Date().toLocaleDateString() + ); + + const [defaultMonth, setDefaultMonth] = useState( + new Date().toLocaleDateString("en-us", { month: "long", year: "numeric" }) + ); + + const [defaultYear, setDefaultYear] = useState(new Date().getFullYear()); + + /* Handlers --------------------------------------------------- */ + + /* Day Handler */ + const handleDay = (day) => { + setDay(day); + setDefaultDay(day); + }; + + /* Week Handler */ + const handleWeek = (week) => { + setWeek(week); + setDefaultWeek(week); + }; + + /* Month Handler */ + const handleMonth = (month) => { + setMonth(month); + setDefaultMonth(month); + }; + + /* Year Handler */ + const handleYear = (year) => { + setYear(year); + setDefaultYear(year); + }; + + /* Calculate start and end of day */ + const startEndOfDay = () => { + // Transforming day value into string + let dayAsString = defaultDay.toString(); + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const dayFormatted = dayAsString.split(search).join(replaceWith); + + // Setting start and end + setStart(dayFormatted); + setEnd(dayFormatted); + }; + + /* Calculate start and end of week */ + const startEndOfWeek = () => { + // Transforming week value into string + let weekAsString = defaultWeek.toString(); + + let weeks = weekAsString.split(","); + + let endOfWeekPlaceholder = moment().isoWeekday(7).format("MM-DD-YYYY"); + + if (weeks.length === 1) { + weeks.push(endOfWeekPlaceholder); + } + + // Replacing "/" with "-" + const search = "/"; + const replaceWith = "-"; + const weekStart = weeks[0].split(search).join(replaceWith); + const weekEnd = weeks[1].split(search).join(replaceWith); + + let weekStartF = moment(weekEnd).isoWeekday(1).format("MM-DD-YYYY"); + let weekEndF = moment(weekEnd).isoWeekday(7).format("MM-DD-YYYY"); + + // Setting start and end + setStart(weekStartF); + setEnd(weekEndF); + }; + + /* Calculate start and end of month */ + const startEndOfMonth = () => { + // Transforming month value into string + let monthAsString = defaultMonth.toString(); + + // Listing all months by number + const months = [ + { name: "January", number: "01" }, + { name: "February", number: "02" }, + { name: "March", number: "03" }, + { name: "April", number: "04" }, + { name: "May", number: "05" }, + { name: "June", number: "06" }, + { name: "July", number: "07" }, + { name: "August", number: "08" }, + { name: "September", number: "09" }, + { name: "October", number: "10" }, + { name: "November", number: "11" }, + { name: "December", number: "12" } + ]; + + // Determining year (number) of month value + let yearOfMonth = monthAsString.slice(-4); + + // Determining month (number) of month value + let monthOfMonth = ""; + + months.forEach((eachMonth) => { + if (monthAsString.includes(eachMonth.name)) { + monthOfMonth += eachMonth.number; + } + }); + + // Completing start of month + let completeMonthStart = `${yearOfMonth}-${monthOfMonth}-01`; + + // Setting up start + let startOfMonth = moment(completeMonthStart) + .startOf("month") + .format("MM-DD-YYYY"); + setStart(startOfMonth); + + // Setting up end + let endOfMonth = moment(startOfMonth).endOf("month").format("MM-DD-YYYY"); + setEnd(endOfMonth); + }; + + /* Calculate start and end of year */ + const startEndOfYear = () => { + // Transforming year value into string + let yearAsString = defaultYear.toString() + + // Calculating start and end + let startOfYear = moment(`01-01-${yearAsString}`).format("MM-DD-YYYY") + let endOfYear = moment(`12-31-${yearAsString}`).format("MM-DD-YYYY") + + // Setting up start and end + setStart(startOfYear) + setEnd(endOfYear) + }; + + /* + Variables with the current start and end values + of day, week month, and year + */ + let currentDay = moment().format("MM-DD-YYYY"); + let currentWeekStart = moment().isoWeekday(1).format("MM-DD-YYYY"); + let currentWeekEnd = moment().isoWeekday(7).format("MM-DD-YYYY"); + let currentMonthStart = moment().startOf("month").format("MM-DD-YYYY"); + let currentMonthEnd = moment().endOf("month").format("MM-DD-YYYY"); + let currentYearStart = moment().startOf("year").format("MM-DD-YYYY"); + let currentYearEnd = moment().endOf("year").format("MM-DD-YYYY"); + + /* + UseEffect for the first render of + the page + */ + useEffect(() => { + setPeriod("Day"); + setStart(currentDay); + setEnd(currentDay); + }, []); + + /* + Use effects that get triggered + when the period value changes + */ + useEffect(() => { + if (period === "Day") { + setStart(currentDay); + setEnd(currentDay); + } else if (period === "Week") { + setStart(currentWeekStart); + setEnd(currentWeekEnd); + } else if (period === "Month") { + setStart(currentMonthStart); + setEnd(currentMonthEnd); + } else if (period === "Year") { + setStart(currentYearStart); + setEnd(currentYearEnd); + } else { + null; + } + }, [period]); + + /* + Use effects that get triggered + when the default values change + */ + useEffect(() => { + startEndOfDay(); + }, [defaultDay]); + + useEffect(() => { + startEndOfWeek(); + }, [defaultWeek]); + + useEffect(() => { + startEndOfMonth(); + }, [defaultMonth]); + + useEffect(() => { + startEndOfYear(); + }, [defaultYear]); + + /* + Function that filters shifts based on the + start and end of the selected date + */ + const filterShifts = () => { + + // Array for filtered shifts + let filteredShifts = []; + + // Keeping shifts that exist within the selected dates + shiftProps?.forEach((shift) => { + let shiftStart = moment(shift.starting_at).format("MM-DD-YYYY"); + let shiftEnd = moment(shift.ending_at).format("MM-DD-YYYY"); + + let shiftYear = shiftStart.slice(-4); + let startAndEndYear = start.slice(-4); + + if (shiftYear === startAndEndYear) { + if ( + shiftStart >= start && + shiftStart <= end && + shiftEnd >= start && + shiftEnd <= end + ) { + filteredShifts.push(shift); + } + } + }); + + // Returning filtered shifts + return filteredShifts; + }; + + // Return ---------------------------------------------------------------------------------------------------- + + return ( +
    +

    Table of Employee Hours

    + {/* Top Column Starts */} +
    + {/* Time period Announcer */} +
    +

    Start: {start}

    +

    End: {end}

    +
    + + {/* Dropdown */} +
    +
    + +
    + + + + +
    +
    +
    + +
    + {/* DatePicker Conditional Rendering Starts */} + <> + {period === "Day" ? ( +
    +
    +

    {`${period}: ${defaultDay}`}

    +
    + + +
    + ) : period === "Week" ? ( +
    +
    +

    {`${period}: ${defaultWeek}`}

    +
    + + +
    + ) : period === "Month" ? ( +
    +
    +

    {`${period}: ${defaultMonth}`}

    +
    + + +
    + ) : period === "Year" ? ( +
    +
    +

    {`${period}: ${defaultYear}`}

    +
    + + +
    + ) : null} + + {/* DatePicker Conditional Rendering Ends */} +
    +
    + {/* Top Column Ends */} + + {/* Bottom Column Starts */} + {/* Table of Employees Starts */} +
    + {workerProps?.map((singleWorker, i) => { + return ( + ) + })} +
    + {/* Table of Employees Ends */} + {/* Bottom Column Ends */} +
    + ); +} \ No newline at end of file diff --git a/src/js/views/metrics/queue/QueueData.js b/src/js/views/metrics/queue/QueueData.js new file mode 100644 index 0000000..93b902e --- /dev/null +++ b/src/js/views/metrics/queue/QueueData.js @@ -0,0 +1,168 @@ +import React from "react"; +import moment from "moment"; +import Avatar from "../../../components/avatar/Avatar"; +import { Button, Theme } from "../../../components/index"; + +// This is needed to render the button "Invite to Shift" +const allowLevels = window.location.search != ""; + +/** + * @function + * @description Creates a table of all employees with their worked/scheduled hours for Queue.js + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires moment + * @requires Avatar + * @requires Button + * @requires Theme + * @param {object} props - Contains an array of all shifts, and an object with information of a single worker, previously mapped in Queue.js + */ +export const QueueData = (props) => { + + //Assigning props to variables ---------------------------- + + // This is a single worker brought from the mapping + // of all workers back in Queue.js + const worker = props.worker; + + // These are all the shifts, with different workers + const shifts = props.shifts; + + // Worker Shifts ------------------------------------------ + + // Array to hold shifts from the single worker + const workerShifts = []; + + // Keeping the shifts whose worker id matches + // the id of the single worker + shifts.forEach((shift) => { + shift.employees.forEach((employee) => { + if (employee === worker.id) { + workerShifts.push(shift); + } + }); + }); + + // Worker Clock-ins --------------------------------------- + + // Array to hold clock-ins from the single worker + let workerClockIns = []; + + // Keeping the clock-ins whose worker id matches + // the id of the single worker + workerShifts.forEach((shift) => { + shift.clockin.forEach((clockIn) => { + if (clockIn.employee === worker.id) { + workerClockIns.push(clockIn); + } + }); + }); + + // Scheduled Hours --------------------------------------- + + // Array to hold scheduled hours from the single worker + let scheduledHours = []; + + // Calculating the scheduled hours of each shift and + // passing that data as an object to "scheduledHours" + workerShifts.forEach((shift) => { + let start = moment(shift.starting_at); + let end = moment(shift.ending_at); + + let diff = moment.duration(end.diff(start)).asHours(); + + scheduledHours.push({ + id: shift.id, + scheduled_hours: diff + }); + }); + + // Adding all the scheduled hours to form a total + let totalScheduledHours = scheduledHours.reduce((accumulator, shift) => { + return accumulator + shift.scheduled_hours; + }, 0); + + // Formatting the total to 2 decimal places + let totalScheduledHoursF = totalScheduledHours.toFixed(2) + + // Worked Hours ---------------------------------------- + + // Array to hold worked hours from the single worker + let workedHours = []; + + // Calculating the worked hours of each shift and + // passing that data as an object to "workedHours" + workerClockIns.forEach((shift) => { + let start = moment(shift.started_at); + let end = moment(shift.ended_at); + + let diff = moment.duration(end.diff(start)).asHours(); + + workedHours.push({ + id: shift.id, + worked_hours: diff + }); + }); + + // Adding all the worked hours to form a total + let totalWorkedHours = workedHours.reduce((accumulator, shift) => { + return accumulator + shift.worked_hours; + }, 0); + + // Formatting the total to 2 decimal places + let totalWorkedHoursF = totalWorkedHours.toFixed(2) + + // Return ------------------------------------------------------------------------------------------------------ + + return ( + <> + + {({ bar }) => ( +
    + {/* Employee Image/Name/Rating Starts */} +
    +
    + +
    +
    +
    {`${worker.user.first_name} ${worker.user.last_name}`}
    +
    {worker.rating == null ? "No rating available" : worker.rating > 1 ? `Rating: ${worker.rating} stars` : `Rating: ${worker.rating} star`}
    +
    +
    + {/* Employee Image/Name/Rating Ends */} + + {/* Scheduled Hours Starts */} +
    +

    {`Scheduled Hours: ${totalScheduledHoursF}`}

    +
    + {/* Scheduled Hours Ends */} + + {/* Worked Hours Starts */} +
    + +

    {`Worked Hours: ${totalWorkedHoursF}`}

    +
    + {/* Worked Hours Ends */} + + {/* Invite Button Starts */} +
    + +
    + {/* Invite Button Ends */} +
    + )} +
    + + ); +}; \ No newline at end of file diff --git a/src/js/views/metrics/ratings/Ratings.js b/src/js/views/metrics/ratings/Ratings.js new file mode 100644 index 0000000..c6881ed --- /dev/null +++ b/src/js/views/metrics/ratings/Ratings.js @@ -0,0 +1,211 @@ +import React, { useEffect, useState } from "react" +import { PieChart } from "../charts" + +/** + * @function + * @description Creates a pie chart and a table reflecting how many job seekers are in each category of star ratings (1 to 5 stars.) + * @since 09.29.22 by Paola Sanchez + * @author Paola Sanchez + * @requires PieChart + * @param {object} props - Contains an array of all shifts, and an array of all workers. + */ +export const Ratings = (props) => { + + // Use state to hold list of workers + const [workersList, setWorkersList] = useState([]) + + // Receiving the props that contain the list of workers + const handleProps = async () => { + + // Catching the props when they arrive + let workersObj = await props + + // Checking length of list before saving it + if (workersObj.workers.length > 0) { + // Saving list of workers + setWorkersList(workersObj.workers) + } else { + // Handling error with a message + console.log("Waiting for props to arrive") + } + } + + // Triggering handleProps when props change/arrive + useEffect(() => { + handleProps() + }, [props]) + + // Rendering based on length of workersList + if (workersList.length > 0) { + + // Preparing the list for the chart data --------------------------------------- + + // Array to hold list of ratings + let ratingsList = [] + + // Gathering ratings of each worker + workersList.forEach((eachWorker) => { + ratingsList.push(eachWorker.rating) + }) + + // Function to make an array of rating quantities + const findQuantities = (passedArray) => { + + // Array to hold rating results + const results = []; + + // Counting how many times each star rating appears + passedArray?.forEach((item) => { + + // Generating indexes + const index = results.findIndex((obj) => { + return obj["rating"] === item; + }); + + // Using the indexes to count rating instances + if (index === -1) { + results.push({ + rating: item, + qty: 1 + }); + + } else { + results[index]["qty"]++; + } + }); + + // Returning array + return results; + }; + + // Generating array of rating quantities + let ratingsQty = findQuantities(ratingsList); + + // Calculating total of all the quantities + let total = ratingsQty.reduce((s, { qty }) => s + qty, 0); + + // Generating and adding percentages as new properties + let ratingsPct = ratingsQty.map(({ rating, qty }) => ({ + rating, + qty, + pct: ((qty * 100) / total).toFixed(0) + })); + + // Organizing objects by numerical order of the "rating" properties + let ratingsFinal = ratingsPct.sort((a, b) => (a.rating - b.rating)) + + // Moving the first object ("Unavailable Rating") to the last position of the array + ratingsFinal.push(ratingsFinal.shift()); + + // Generating an object with the totals + let totalsObj = { rating: "Total Employees", qty: total, pct: "100" } + + // Adding object with totals to the array + ratingsFinal.push(totalsObj) + + // Adding id's to every object in the array + ratingsFinal.forEach((item, i) => { + item.id = i + 1; + }); + + // Preparing the chart data ---------------------------------------------------- + + // Colors + const purple = "#5c00b8"; + const lightTeal = "#00ebeb"; + const darkTeal = "#009e9e"; + const green = "#06ff05"; + const lightPink = "#eb00eb"; + const darkPink = "#b200b2"; + + // Taking out the "Totals" from the pie chart view + let pieData = ratingsFinal.filter((item) => { return item.rating !== "Total Employees" }) + + // Preparing data to be passed to the chart component + const ratingsData = { + labels: pieData.map((data) => { return data.rating === null ? "Unavailable Rating" : ` ${data.rating} Star Employees` }), + datasets: [{ + label: "Employee Ratings", + data: pieData.map((data) => data.qty), + backgroundColor: [ + green, darkTeal, lightPink, + purple, lightTeal, darkPink + ], + }] + } + + // Return ---------------------------------------------------------------------- + + return ( +
    + {/* Left Column Starts */} +
    +
    + {/* Ratings Table Starts */} +
    +

    Employee Ratings Table

    + + + + {/* Table columns */} + + + + + + + + + {/* Mapping the data to diplay it as table rows */} + {ratingsFinal.map((item, i) => { + return item.rating === null ? ( + + + + + + ) : item.rating === "Total Employees" ? ( + + + + + + ) : ( + + + + + + ) + })} + +

    Star Rating

    Quantity

    Percentages

    Unavailable Rating

    {item.qty}

    {`${item.pct}%`}

    {item.rating}

    {item.qty}

    {`${item.pct}%`}

    {`${item.rating} Star Employees`}

    {item.qty}

    {`${item.pct}%`}

    +
    + {/* Ratings Table Ends */} +
    +
    + {/* Left Column Ends */} + + {/* Right Column Starts */} +
    +
    + {/* Ratings Chart Starts */} +
    +

    Employee Ratings Chart

    + +
    + +
    +
    + {/* Ratings Chart Ends */} +
    +
    + {/* Right Column Ends */} +
    + ) + } else { + return ( +

    Loading

    + ) + } +} \ No newline at end of file diff --git a/src/js/views/payroll.js b/src/js/views/payroll.js index f54b9d9..9ab2935 100644 --- a/src/js/views/payroll.js +++ b/src/js/views/payroll.js @@ -4464,7 +4464,7 @@ export class PayrollReport extends Flux.DashView { return ( diff --git a/src/js/views/profile.js b/src/js/views/profile.js index a2fec79..8e89856 100644 --- a/src/js/views/profile.js +++ b/src/js/views/profile.js @@ -258,7 +258,6 @@ export class Profile extends Flux.DashView { this.cropper = cropper; } callback = (data) => { - console.log("DATA", data); // if(data.action == 'next' && data.index == 0){ // this.props.history.push("/payroll"); diff --git a/src/js/views/shift.test.js b/src/js/views/shift.test.js deleted file mode 100644 index bab9060..0000000 --- a/src/js/views/shift.test.js +++ /dev/null @@ -1,69 +0,0 @@ -import React from 'react'; -import { ReactDOM } from 'react-dom'; - -// const Shift = require('./shifts.js'); - -// test('adds 1 + 2 to equal 3', () => { -// expect(sum(1, 2)).toBe(3); -// }); -const Shift = require('./shifts.js'); -// import {Shift} from "./shifts.js"; -jest.mock('./shifts.js'); -const mockFn = jest.fn(); -const mockMethod = jest.fn(); -const a = new mockFn(); - -Shift.withStatus(() => { - return { - method: mockMethod, - }; - }); -const some = new Shift(); -some.method('a', 'b'); -console.log('Calls to method: ', mockMethod.mock.calls); -test('Creating a shift with: no recurrent, single date, anyone', () => { - expect(Shift({ - allowedFavlists: [], - allowedTalents: [], - allowed_from_list: [], - application_restriction: "ANYONE", - candidates: [], - description: "N/A", - employees: [], - employer: 56, - ending_at: "2022-06-29T20:15:47.883Z", - has_sensitive_updates: true, - maximum_allowed_employees: "1", - minimum_allowed_rating: "0", - minimum_hourly_rate: "8", - pending_invites: [], - pending_jobcore_invites: [], - position: "3", - starting_at: "2022-06-29T18:15:47.883Z", - status: "OPEN", - venue: "12" - }).defaults()).toBe([{ - "allowedFavlists": [], - "allowedTalents": [], - "allowed_from_list": [], - "application_restriction": "ANYONE", - "candidates": [], - "description": "N/A", - "employees": [], - "employer": 56, - "ending_at": "2022-06-29T20:15:47.883Z", - "has_sensitive_updates": true, - "maximum_allowed_employees": "1", - "minimum_allowed_rating": "0", - "minimum_hourly_rate": "8", - "pending_invites": [], - "pending_jobcore_invites": [], - "position": "3", - "serialize": a, - "starting_at": "2022-06-29T18:15:47.883Z", - "status": "OPEN", - "unserialize": a, - "venue": "12", - "withStatus": a - }]) -}) \ No newline at end of file diff --git a/src/js/views/talents.js b/src/js/views/talents.js index 410bea2..39be432 100644 --- a/src/js/views/talents.js +++ b/src/js/views/talents.js @@ -256,7 +256,7 @@ export class ManageTalents extends Flux.DashView { } } const today = new Date() - console.log("empleados#########", employees.map(checkEmployability)) // leave this one for now [Israel] + //console.log("empleados#########", employees.map(checkEmployability)) // leave this one for now [Israel] const positions = this.state.positions; if (this.state.firstSearch) return

    Please search for an employee

    ; const allowLevels = window.location.search != ""; @@ -508,6 +508,9 @@ FilterTalents.propTypes = { /** * Talent Details + * + * Before, the Stars component was rendered inside a p tag, + * now its rendered inside a span tag */ export const TalentDetails = (props) => { const employee = props.catalog.employee; @@ -544,12 +547,12 @@ export const TalentDetails = (props) => { /> -

    + -

    +

    {typeof employee.fullName == "function" ? employee.fullName() diff --git a/src/styles/_icons.scss b/src/styles/_icons.scss index c125435..fbdb21d 100644 --- a/src/styles/_icons.scss +++ b/src/styles/_icons.scss @@ -14,6 +14,7 @@ &.icon-shifts{ background-image: url('../img/icons/shifts_color.png'); } &.icon-talents{ background-image: url('../img/icons/talents_color.png'); } &.icon-logout{ background-image: url('../img/icons/logout_color.png'); } + &.icon-metrics{ background-image: url('../img/icons/metrics_color.png'); } } .svg_img{