Spamworldpro Mini Shell
Spamworldpro


Server : Apache
System : Linux server2.corals.io 4.18.0-348.2.1.el8_5.x86_64 #1 SMP Mon Nov 15 09:17:08 EST 2021 x86_64
User : corals ( 1002)
PHP Version : 7.4.33
Disable Function : exec,passthru,shell_exec,system
Directory :  /home/corals/ts.corals.io/frontend/components/Clients/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Current File : /home/corals/ts.corals.io/frontend/components/Clients/BillableEntries.vue
<template>
  <div>
    <template v-if="!isReady">
      <div class="loader">
        <h4 class="text-center text-muted">
          Loading
          <span class="dot">.</span>
          <span class="dot">.</span>
          <span class="dot">.</span>
        </h4>
      </div>
    </template>
    <template v-else>
      <div v-if="getProjects.length && $isAdmin()" class="mb-4">
        <b-form-checkbox
          id="include_prev"
          class="d-inline-block"
          v-model="includeThePrev"
          name="include_prev"
          :value="1"
          :unchecked-value="0"
        >
          On Generate Invoice include not billed entries from previous cycles
        </b-form-checkbox>
      </div>
      <div class="row">
        <div class="col-md-8">
          <div class="form-group form-row">
            <div class="col-md-6">
              <b-form-input v-model.trim="search" class="form-control" placeholder="Type to filter..." debounce="500"/>
            </div>
            <div class="col-md-2">
              <b-button :disabled="!search" @click="search = ''">Clear</b-button>
            </div>
          </div>
        </div>
        <div class="col-md-4 text-right" v-if="showGenerateClientInvoiceBtn">
          <b-button @click.prevent="generateInvoice" variant="primary" v-if="$isAdmin()">
            <fa icon="plus"/>
            Generate Invoice
          </b-button>
        </div>
      </div>
      <template v-for="(project,index) in getProjects">
        <h4>{{ project.name }}</h4>
        <div class="accordion" role="tablist">
          <template v-for="(cycle,code) in project.cycles">
            <b-card no-body class="card-overflow mb-1">
              <b-card-header header-tag="header" class="p-1" role="tab">
                <b-button block v-b-toggle="'accordion-'+index+code"
                          :variant="cycle.is_current?'secondary':'success'" class="d-flex justify-content-between">
                  <div class="align-self-center">{{ cycle.start }} - {{ cycle.end }}</div>
                  <div class="alert alert-warning p-1 m-0" v-if="cycle.has_unreviewed_entries">
                    Some of entries are pending review!
                  </div>
                  <div>
                    <b class="mx-3">
                      Total: {{ cycle.total_evaluation_time }}
                    </b>
                    <div class="dropdown d-inline-block" v-isAdmin>
                      <b-dropdown id="dropdown" dropleft no-caret variant="warning" toggle-class="p-1" size="sm">
                        <template #button-content>
                          <fa icon="ellipsis-v"/>
                        </template>
                        <a href="#" class="btn btn-sm dropdown-item"
                           @click.stop="generateInvoicePerCycle(cycle,project)">
                          <fa icon="file-invoice"/>
                          Generate Pending Invoice
                        </a>
                        <a href="#" class="btn btn-sm dropdown-item"
                           @click.stop="generateVoidInvoicePerCycle(cycle,project)">
                          <fa icon="file-invoice"/>
                          Generate Void Invoice
                        </a>
                        <a href="#" class="btn btn-sm dropdown-item"
                           @click.stop="generatePaidInvoicePerCycle(cycle,project)">
                          <fa icon="file-invoice"/>
                          Generate Paid Invoice
                        </a>
                      </b-dropdown>
                    </div>
                  </div>
                </b-button>
              </b-card-header>
              <b-collapse :id="'accordion-'+index+code" :accordion="'cycle-accordion-'+index+code" role="tabpanel">
                <b-card-body>
                  <div class="table-responsive">
                    <table class="VueTables__table table">
                      <thead>
                      <tr>
                        <th class="cw-120">Date</th>
                        <th class="cw-200">Resource</th>
                        <th>Description</th>
                        <th class="cw-120">Time</th>
                        <th v-isAdmin>Actions</th>
                      </tr>
                      </thead>
                      <tbody>
                      <template v-for="(entryGroup,gIndex) in cycle.entries">
                        <template v-for="(entry,eIndex) in entryGroup">
                          <tr :class="gIndex%2?'odd-row':'even-row'">
                            <td :rowspan="entryGroup.length" v-if="eIndex === 0">
                              {{ entry.spent_at }}
                              <br/>
                              <small>{{ entry.spent_at_day.name }}</small></td>
                            <td :rowspan="userCount(entryGroup,entry.user)"
                                v-if="eIndex===0? true:entryGroup[eIndex-1].user === entryGroup[eIndex].user? false:true">
                              <div>{{ entry.user }}</div>
                              <div class="font-weight-bold mt-1">
                                {{ totalUserTime(entryGroup, entry.user) }}
                              </div>
                            </td>
                            <td>
                              <div v-html="$getTextWithLinks(entry.description)"></div>
                              <div>
                                <template v-for="label in entry.labels">
                                  <span class="badge badge-warning mr-1">{{ label.name }}</span>
                                </template>
                              </div>
                            </td>
                            <th>
                              {{ entry.evaluation_time }}
                              <fa icon="check-circle" v-if="entry.has_reviewed" v-b-tooltip="'Has Reviewed'"
                                  class="text-success"/>
                            </th>

                            <th v-isAdmin>
                              <b-link @click.prevent="showEditEntryModal(entry)" v-b-tooltip.hover title="Edit">
                                <fa icon="edit"/>
                              </b-link>
                              <b-link class="text-danger" @click.prevent="deleteEntry(entry)"
                                      v-b-tooltip.hover
                                      title="Delete">
                                <fa icon="trash"/>
                              </b-link>
                            </th>
                          </tr>
                        </template>
                      </template>
                      </tbody>
                    </table>
                  </div>
                </b-card-body>
              </b-collapse>
            </b-card>
          </template>
          <h4 v-if="!Object.keys(project.cycles).length" class="text-center text-muted">No data available</h4>
        </div>
        <hr/>
      </template>
      <h4 v-if="!getProjects.length" class="text-center text-muted">No data available</h4>
    </template>
    <b-modal hide-footer
             content-class="shadow"
             title="Edit Entry"
             no-close-on-backdrop :id="formModalId" @hidden="modelHidden"
             @show="onModalShow" size="lg">
      <c-overlay :show="!form.isReady">
        <form @submit.prevent="submitEditEntryForm">

          <entry-fields :form="form"/>

          <div class="text-right">
            <button type="submit" class="btn btn-sm btn-primary" :disabled="!form.isReady">
              Update Entry
            </button>
            <button @click.prevent="$bvModal.hide(formModalId)" class="btn btn-sm btn-secondary"
                    :disabled="!form.isReady">Close
            </button>
          </div>
        </form>
      </c-overlay>
    </b-modal>
  </div>
</template>

<script>

import COverlay from "@/components/layout/COverlay";
import EntryFields from "@/components/Entries/EntryFields";

export default {
  name: "ClientsBillableEntriesIndex",
  components: {EntryFields, COverlay},
  props: {
    client: {
      required: true
    }
  },
  async fetch() {
    this.projects = await this.fetchProjects();
  },
  data() {
    return {
      showGenerateClientInvoiceBtn: false,
      formModalId: 'entry-cycle-modal',
      projects: [],
      ready: false,
      selectedEntry: null,
      includeThePrev: 1,
      search: '',
      form: this.$form({
        activity_id: '',
        user_id: '',
        project_id: '',
        spent_at: new Date(),
        hours: '',
        minutes: '',
        description: '',
        evaluation_minutes: '',
        evaluation_hours: '',
        has_reviewed: 0,
      }, {fetchFormDataURL: 'timesheet/entries/get-form-data', model: 'entry'})
    }
  },
  methods: {
    onModalShow() {
      if (this.selectedEntry) {
        return;
      }

      this.form.replace(this.form.originalData);
      this.selectedEntry = null;
      this.form.errors.purge();
    },
    modelHidden() {
      this.form.replace(this.form.originalData);
      this.selectedEntry = null;
      this.form.errors.purge();
    },
    submitEditEntryForm() {
      this.form.put(`timesheet/entries/${this.selectedEntry.id}`)
        .then(async (response) => {
          this.$bvModal.hide(this.formModalId);
          this.projects = await this.fetchProjects();

        });
    },
    showEditEntryModal(entry) {
      this.selectedEntry = entry;

      this.$axios
        .get(`timesheet/entries/${entry.id}?edit=1`)
        .then(({data: record}) => {
          this.form.replace(record.data);
          this.$bvModal.show(this.formModalId);
        }).catch(err => {
        this.$toast.error(err.message);
      });
    },
    deleteEntry(entry) {
      this.$swal.fire({
        title: 'Are you sure?',
        text: "You won't be able to revert this!",
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#d33',
        cancelButtonColor: '#d7d7d7',
        confirmButtonText: 'Yes, delete it!'
      }).then((result) => {
        if (result.value) {
          this.doDeleteEntry(entry);
        }
      })
    },
    doDeleteEntry(entry) {
      this.$axios.delete(`timesheet/entries/${entry.id}`)
        .then(async response => {
          this.projects = await this.fetchProjects();
        }).catch(err => {
        let message = err.message;
        if (err.response && err.response.data && err.response.data.message) {
          message = err.response.data.message;
        }
        this.$toast.error(message);
      })
    },
    generateInvoicePerCycle(cycle, project, status = 'pending') {
      if (this.$notAdmin()) {
        return;
      }
      this.$swal.fire({
        title: 'Are you sure?',
        text: "You are going to generate invoice for this cycle.",
        icon: 'warning',
        showCancelButton: true,
        confirmButtonColor: '#007bff',
        cancelButtonColor: '#d7d7d7',
        confirmButtonText: 'Generate'
      }).then((result) => {
        if (result.value) {
          this.$axios.post(`timesheet/entries/generate-invoice-per-cycle/${project.id}/${this.client.id}?status=${status}`, {
            cycle_start: cycle.start,
            cycle_end: cycle.end,
            include_previous_entries: this.includeThePrev
          }).then(async response => {
            this.$toast.success(response.data.message);
            this.projects = await this.fetchProjects();
          }).catch(err => {
            console.error(err);
          })
        }
      })
    },
    generateVoidInvoicePerCycle(cycle, project) {
      if (this.$notAdmin()) {
        return;
      }
      this.generateInvoicePerCycle(cycle, project, 'void')
    },
    generatePaidInvoicePerCycle(cycle, project) {
      if (this.$notAdmin()) {
        return;
      }
      this.generateInvoicePerCycle(cycle, project, 'paid')
    },
    async fetchProjects() {
      this.ready = false;
      this.showGenerateClientInvoiceBtn = false;

      let result = await this.$axios.get(`timesheet/entries/entries-per-billing-cycle/${this.client.id}`)
        .then(({data}) => {
          this.ready = true;
          return data.data;
        });

      result.forEach((project) => {
        for (let cycle in project.cycles) {
          if (project.cycles[cycle].is_current === false) {
            this.showGenerateClientInvoiceBtn = true;
          }
        }
      });

      return result
    },

    userCount(entryGroup, user) {
      var count = 0;
      entryGroup.forEach((arr) => (arr.user === user && count++));
      return count;
    },
    totalUserTime(entryGroup, user) {
      var sum = 0;
      var hours = 0;
      var minutes = 0;
      entryGroup.forEach((arr) => {
        if (arr.user === user) {
          hours += arr.evaluation_hours;
          minutes += arr.evaluation_minutes;
        }
      });
      if (minutes >= 60) {
        hours += Math.floor(minutes / 60);
        minutes = minutes % 60
      }
      if (minutes < 10) {
        minutes = "0" + minutes;
      }
      if (hours < 10) {
        hours = "0" + hours;
      }
      sum = hours + ':' + minutes
      return sum;
    },
    totalEvaluationTime(entries) {
      var hoursAndMinutesTime = [];
      var evaluationHours = 0;
      var evaluationMinutes = 0;

      for (var i = 0; i < entries.length; i++) {
        for (var j = 0; j < entries[i].length; j++) {
          evaluationHours = evaluationHours + entries[i][j].evaluation_hours;
          evaluationMinutes = evaluationMinutes + entries[i][j].evaluation_minutes;
        }
      }

      if (evaluationMinutes >= 60) {
        evaluationHours += Math.floor(evaluationMinutes / 60);
        evaluationMinutes = evaluationMinutes % 60
      }
      if (evaluationMinutes < 10) {
        evaluationMinutes = "0" + evaluationMinutes;
      }
      if (evaluationHours < 10) {
        evaluationHours = "0" + evaluationHours;
      }
      hoursAndMinutesTime['total_evaluation_hours'] = evaluationHours;
      hoursAndMinutesTime['total_evaluation_minutes'] = evaluationMinutes;

      return hoursAndMinutesTime;
    },

    generateInvoice() {
      this.$axios.post(`timesheet/clients/${this.client.id}/generate-invoice`)
        .then(async response => {
          this.$toast.success(response.data.message);
          this.projects = await this.fetchProjects();
        }).catch(err => {
        this.$toast.error(err.message);
      })
    }
  },
  computed: {
    getProjects() {
      if (!this.search.length) {
        return this.projects;
      }

      let result = [];

      this.projects.forEach((project) => {
        let cycles = {};
        let isEmpty = 0;
        let projectFilter = [];
        for (let cycle in project.cycles) {
          let entries = [];
          let eachCycle = {};
          for (let k = 0; k < project.cycles[cycle].entries.length; k++) {
            let entry = [];

            entry = project.cycles[cycle].entries[k].filter(arr => {
              return (arr.user.toLowerCase().includes(this.search.toLowerCase())) ||
                (arr.description.toLowerCase().includes(this.search.toLowerCase())) ||
                (arr.spent_at.includes(this.search))
            });

            if (entry.length > 0) {
              entries.push(entry);
            }
          }
          if (entries.length > 0) {
            let hoursAndMinutesTime = this.totalEvaluationTime(entries);
            eachCycle = {
              'code': project.cycles[cycle].code,
              'end': project.cycles[cycle].end,
              'start': project.cycles[cycle].start,
              'is_current': project.cycles[cycle].is_current,
              'entries': entries,
              'total_evaluation_hours': hoursAndMinutesTime['total_evaluation_hours'],
              'total_evaluation_minutes': hoursAndMinutesTime['total_evaluation_minutes'],
              'total_evaluation_time': hoursAndMinutesTime['total_evaluation_hours'] + ':' + hoursAndMinutesTime['total_evaluation_minutes'],
            };
          } else {
            eachCycle = [];
          }

          if (eachCycle.length !== 0) {
            isEmpty = 1;
            cycles[project.cycles[cycle].code] = eachCycle;
          }
        }
        if (isEmpty === 1) {
          projectFilter = {
            'budget': project.budget,
            'currency': project.currency,
            'cycles': cycles,
            'hourly_rate': project.hourly_rate,
            'id': project.id,
            'name': project.name,

          };
          result.push(projectFilter);
        }

      });
      return result;
    },
    isReady() {
      return this.ready;
    }
  }
}
</script>

<style scoped>

.card-overflow {
  overflow-x: visible !important;
  overflow-y: visible !important;
}
</style>

Spamworldpro Mini