![]() 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/ |
<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>