Added dockerfile

This commit is contained in:
Ali Arya 2025-05-07 19:19:14 +03:30
parent e4868deb16
commit 62858f3aba
17 changed files with 1510 additions and 265 deletions

1
DOC.txt Normal file
View File

@ -0,0 +1 @@
latest origin: 3

33
Dockerfile Normal file
View File

@ -0,0 +1,33 @@
# Use a Node.js image for the build stage
FROM node:14 AS build
# Set the working directory
WORKDIR /app
# Copy package.json and package-lock.json (if available)
COPY package.json ./
# COPY package-lock.json ./ # Uncomment if you have a package-lock.json
# Install dependencies
RUN npm install
# Copy the rest of the application code
COPY . .
# Build the application
RUN npm run build
# Use the busybox image for the final stage
FROM busybox:latest
# Set the working directory
WORKDIR /app
# Copy the built assets from the build stage
COPY --from=build /app/dist ./
# Expose the port the app runs on
EXPOSE 80
# Command to serve the static files
CMD ["httpd", "-f", "-p", "80"]

BIN
dist.zip Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -256,6 +256,13 @@ body {
/******************************************************************************/
/* Overwriting 's Original Styles */
/******************************************************************************/
// .ant-table td { white-space: nowrap; }

View File

@ -0,0 +1,115 @@
<template>
<b-modal id="modal-show-plan" v-bind:centered="true" size="lg">
<!-- Header -->
<template v-slot:modal-header="{ close }">
<h2 class="text-primary m-0">Plan's Information</h2>
<img style="cursor: pointer;" src="@/assets/svg/cross.svg" alt="close-icon" v-on:click="close()">
</template>
<!-- Body -->
<template v-if="data" v-slot:default class="d-flex flex-column" style="">
<b-row class="mb-2">
<b-col cols="12">
<h3>Plan: {{ data.title }}</h3>
</b-col>
<b-col cols="12">
<span class="info-key">Description:</span>
<span class="info-value">{{ data.description }}</span>
</b-col>
</b-row>
<b-row style="row-gap: 1rem;">
<b-col md="6" xl="4">
<span class="info-key">Start Date:</span>
<span class="info-value">{{ GetDate(data.from_date) }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">End Date:</span>
<span class="info-value">{{ GetDate(data.to_date) }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Months:</span>
<span class="info-value">{{ data.months }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">User Limit:</span>
<span class="info-value">{{ data.attributes["user limit"] }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Storage Limit:</span>
<span class="info-value">{{ FormatBytes(data.storage_limit) }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Is Active:</span>
<span class="info-value">
<svg v-if="data.is_active" style="width: 24px; height: 24px;" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
<svg v-else style="width: 24px; height: 24px;" viewBox="0 0 24 24">
<path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</span>
</b-col>
</b-row>
</template>
<!-- Footer -->
<template v-slot:modal-footer="{ hide }">
<a class="text-primary" v-on:click="hide()">Close</a>
</template>
</b-modal>
</template>
<script>
import { BForm, BButton, BRow, BCol } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import { SeparateNumberByThousands, FormatBytes } from '@/modules/core'
export default {
name: 'AddUserModalComponent',
props: {
data: {
type: Object,
required: false,
},
},
components: {
BForm,
BButton,
BRow,
BCol,
},
directives: {
Ripple,
},
data() {
return {
}
},
methods: {
FormatBytes(Bytes)
{
return FormatBytes(Bytes)
},
GetDate(DateTimeString)
{
return new Date(DateTimeString).toDateString()
},
},
}
</script>
<style scoped lang="scss">
.info-key {
font-weight: 500;
font-size: 16px;
}
.info-value {
margin-inline-start: 0.5rem;
}
</style>

View File

@ -1,32 +1,31 @@
<template>
<b-sidebar
id="user-filters--sidebar"
bg-variant="white"
v-model="IsOpen_D"
title="Filters"
v-bind:width="SidebarWidth"
body-class="p-2"
shadow
backdrop
right
>
<transition>
<!-- No Filters -->
<!-- <p v-if="Filters_D.length === 0" class="text-center text-dark" style="font-size: 1.125rem; font-weight: 500;">
No filters are specified.
</p> -->
<!-- For the documentation, go to the end of the file. -->
<b-form
ref="filters"
id="filters-repeater-form"
v-bind:style="{ height: trHeight }"
v-on:submit.prevent="Repeat()"
>
<b-row
ref="row"
v-for="(Filter, Index) in Filters_D"
v-bind:key="Index"
>
<template>
<b-sidebar id="user-filters--sidebar"
bg-variant="white"
v-model="IsOpen_D"
title="Filters"
v-bind:width="SidebarWidth"
body-class="p-2"
shadow
backdrop
right>
<!-- No Filters -->
<!-- commented out because animations are buggy and don't work properly -->
<!-- <p v-if="Filters_D.length === 0"
class="text-center text-secondary"
style="font-size: 1.125rem; font-weight: 500;">
You have not filtered anything.
</p> -->
<transition>
<b-form ref="filters"
id="filters-repeater-form"
v-bind:style="{ height: trHeight }"
v-on:submit.prevent="Repeat()">
<b-row ref="row"
v-for="(Filter, Index) in Filters_D"
v-bind:key="Index">
<b-col lg="3">
<b-form-group label="Field">
<v-select
@ -38,10 +37,11 @@
/>
</b-form-group>
</b-col>
<b-col lg="3">
<b-form-group label="Condition" v-if="Filter.Field.Type !== 'boolean'">
<v-select
v-bind:options="Conditions_D"
v-bind:options="GetConditions(Filter.Field.Value)"
label="Title"
v-bind:clearable="false"
v-bind:value="Filter.Condition"
@ -49,32 +49,47 @@
/>
</b-form-group>
</b-col>
<!-- $COLUMN_VALUES -->
<b-col lg="4">
<b-form-group label="Value" v-if="Filter.Condition.Value !== 'isnull'">
<b-form-checkbox
v-if="Filter.Field.Type === 'boolean'"
style="user-select: none;"
v-model="Filter.Value"
v-bind:value="true"
v-bind:unchecked-value="false"
>
<b-form-checkbox v-if="Filter.Field.Type === $TYPES.Bool"
style="user-select: none;"
v-model="Filter.Value"
v-bind:value="true"
v-bind:unchecked-value="false">
</b-form-checkbox>
<b-form-input
v-else-if="Filter.Field.Type === 'integer'"
type="number"
v-model="Filter.Value"
/>
<b-form-input v-else-if="Filter.Field.Type === $TYPES.Int"
type="number"
v-model="Filter.Value" />
<v-select v-else-if="Filter.Field.Type === $TYPES.Status"
v-bind:options="$STATUS_VALUES"
label="Title"
v-bind:clearable="false"
v-bind:value="Filter.Value"
v-on:input="Filter.Value = $event" />
<b-form-datepicker v-else-if="Filter.Field.Type === $TYPES.Date"
v-model="Filter.Value"
v-bind:date-format-options="{ day: 'numeric', month: 'long', year: 'numeric' }"
today-button
close-button
label-help=""
nav-button-variant="primary" />
<b-form-input v-else type="text" v-model="Filter.Value" />
</b-form-group>
</b-col>
<!-- Delete Button -->
<b-col lg="2" class="mb-1">
<b-form-group label=" ">
<b-button
v-ripple.400="'rgba(234, 84, 85, 0.15)'"
variant="outline-danger"
class="mt-0 mt-md-2"
v-on:click="Remove(Index)"
>
<b-button v-ripple.400="'rgba(234, 84, 85, 0.15)'"
variant="outline-danger"
class="mt-0 mt-md-2"
v-on:click="Remove(Index)">
<feather-icon icon="XIcon" class="mr-25" />
<span>Delete</span>
</b-button>
@ -130,11 +145,46 @@
</template>
<script>
import { BSidebar, BRow, BCol, BCard, BButton, BForm, BFormGroup, BFormInput, BFormCheckbox } from 'bootstrap-vue'
import { BSidebar, BRow, BCol, BCard, BButton, BForm, BFormGroup, BFormInput, BFormCheckbox, BFormDatepicker } from 'bootstrap-vue'
import vSelect from 'vue-select'
import Ripple from 'vue-ripple-directive'
import { heightTransition } from '@core/mixins/ui/transition'
import { Log } from '@/modules/core'
// -----------------------------------------------------------------------------
// CONSTANTS
// -----------------------------------------------------------------------------
const $CONDITIONS = {
// IsNull: { Value: 'isnull', Title: 'Is Empty' },
Eq: { Value: 'eq', Title: 'Equals' },
NotEq: { Value: 'neq', Title: 'Not Equals' },
Gt: { Value: 'gt', Title: 'Greater Than' },
GtEq: { Value: 'gte', Title: 'Greater Than or Equals' },
Lt: { Value: 'lt', Title: 'Less Than' },
LtEq: { Value: 'lte', Title: 'Less Than or Equals' },
// Contains: { Value: 'in', Title: 'Contains' },
}
const $TYPES = {
Int: 'integer', // @notimplemented
Bool: 'boolean',
Str: 'string',
Percent: 'percent', // @notimplemented
Date: 'date', // @notimplemented
Time: 'time', // @notimplemented
DateTime: 'datetime', // @notimplemented
Status: 'enum@status',
}
const $STATUS_VALUES = [
{ Value: '1', Title: 'Created' },
{ Value: '2', Title: 'Active' },
{ Value: '3', Title: 'Inactive' },
{ Value: '4', Title: 'Expired' }
]
// -----------------------------------------------------------------------------
// VUE INSTANCE
// -----------------------------------------------------------------------------
export default {
name: 'UserFiltersRepeatingFormComponent',
@ -149,6 +199,7 @@ export default {
BFormGroup,
BFormInput,
BFormCheckbox,
BFormDatepicker,
vSelect,
},
@ -162,24 +213,29 @@ export default {
return {
IsOpen_D: false,
Filters_D: [],
RemainingTableColumns_D: [
{ Value: 'first_name', Type: 'string', Title: 'First Name' },
{ Value: 'email_address', Type: 'string', Title: 'Email' },
{ Value: 'is_main', Type: 'boolean', Title: 'Firm' },
{ Value: 'subscription__charge', Type: 'integer', Title: 'Charge' },
{ Value: 'total_uploaded_size', Type: 'integer', Title: 'Uploaded Size' },
{ Value: 'total_firm_uploaded_size', Type: 'integer', Title: 'Firm Uploaded Size' },
],
Conditions_D: [
{ Value: 'isnull', Title: 'Is Empty' },
{ Value: '=', Title: 'Equals' },
{ Value: '!=', Title: 'Not Equals' },
{ Value: '>', Title: 'Greater Than' },
{ Value: '>=', Title: 'Greater Than or Equals' },
{ Value: '<', Title: 'Less Than' },
{ Value: '<=', Title: 'Less Than or Equals' },
{ Value: 'in', Title: 'Contains' },
{ Value: 'status', Type: $TYPES.Status, Title: 'Status' },
{ Value: 'from_date', Type: $TYPES.Date, Title: 'Start Date' },
{ Value: 'to_date', Type: $TYPES.Date, Title: 'End Date' },
// { Value: 'first_name', Type: $TYPES.Str, Title: 'First Name' },
// { Value: 'email_address', Type: $TYPES.Str, Title: 'Email' },
// { Value: 'is_main', Type: $TYPES.Bool, Title: 'Firm' },
// { Value: 'subscription__charge', Type: $TYPES.Int, Title: 'Charge' },
// { Value: 'total_uploaded_size', Type: 'integer', Title: 'Uploaded Size' },
// { Value: 'total_firm_uploaded_size', Type: 'integer', Title: 'Firm Uploaded Size' },
],
// Conditions_D: [
// // { Value: 'isnull', Title: 'Is Empty' },
// { Value: 'eq', Title: 'Equals' },
// { Value: '!=', Title: 'Not Equals' },
// { Value: '>', Title: 'Greater Than' },
// { Value: '>=', Title: 'Greater Than or Equals' },
// { Value: '<', Title: 'Less Than' },
// { Value: '<=', Title: 'Less Than or Equals' },
// // { Value: 'in', Title: 'Contains' },
// ],
}
},
@ -194,7 +250,7 @@ export default {
},
methods: {
// Initializes the height of the transition
// Initializes the height of the transition.
InitTrHeight()
{
this.trSetHeight(null)
@ -203,11 +259,12 @@ export default {
})
},
// Adds a new row to the filters form repeater
// Adds a new row to the filters form repeater.
Repeat()
{
const FieldType = this.RemainingTableColumns_D[0].Type
const Condition = FieldType === 'boolean' ? this.Conditions_D[1] : this.Conditions_D[0]
// const Condition = FieldType === 'boolean' ? $CONDITIONS.Eq : $CONDITIONS.IsNull
const Condition = $CONDITIONS.Eq
this.Filters_D.push({
Field: this.RemainingTableColumns_D[0],
Condition,
@ -223,7 +280,7 @@ export default {
})
},
// Deletes a row from the filters form repeater
// Deletes a row from the filters form repeater.
Remove(Index)
{
this.RemainingTableColumns_D.push(this.Filters_D[Index].Field)
@ -232,25 +289,39 @@ export default {
this.trTrimHeight(this.$refs.row[0].offsetHeight)
},
// Removes all filters
// Removes all filters.
Clear()
{
this.Filters_D.forEach(Item => this.RemainingTableColumns_D.push(Item.Field))
this.RemainingTableColumns_D.sort((a, b) => a.Title.localeCompare(b.Title))
this.Filters_D = []
this.InitTrHeight()
this.$nextTick(() => {
this.trSetHeight(0)
})
// this.InitTrHeight()
},
// Applies the filters to the table
// Applies the filters to the table.
ApplyFilters()
{
this.$emit('submit', this.Filters_D.map(Item => {
var Arguments = this.Filters_D.map(Item => {
return {
field: Item.Field.Value,
condition: Item.Condition.Value,
value: Item.Value,
// Amini Filters:
// col: Item.Field.Value,
// op: Item.Condition.Value,
// val: Item.Value,
// Gheychisaz Filters:
type: Item.Field.Value,
opr: Item.Condition.Value,
val: (typeof Item.Value === 'object') ? Item.Value.Value : Item.Value,
}
}))
})
Arguments = Arguments[0]
this.$emit('submit', Arguments)
this.IsOpen_D = false
// if( this.Query_D.IsMain )
// this.table.query.query.push({
@ -265,11 +336,13 @@ export default {
{
this.RemainingTableColumns_D.push(this.Filters_D[Index].Field)
this.Filters_D[Index].Field = Value
this.Filters_D[Index].Condition = Value.Type === 'boolean' ? this.Conditions_D[1] : this.Conditions_D[0]
// this.Filters_D[Index].Condition = Value.Type === 'boolean' ? $CONDITIONS.Eq : $CONDITIONS.IsNull
this.Filters_D[Index].Condition = $CONDITIONS.Eq
this.Filters_D[Index].Value =
Value.Type === 'boolean' ? false :
Value.Type === 'integer' ? 0 :
Value.Type === 'string' ? '' : undefined,
Value.Type === 'string' ? '' :
Value.Type === $TYPES.Status ? $STATUS_VALUES[0] : undefined,
this.RemainingTableColumns_D = this.RemainingTableColumns_D.filter(Item => Item.Value !== Value.Value)
this.RemainingTableColumns_D.sort((a, b) => a.Title.localeCompare(b.Title))
},
@ -283,13 +356,34 @@ export default {
this.Filters_D[Index].Value !== undefined ? this.Filters_D[Index].Value :
this.Filters_D[Index].Field.Type === 'boolean' ? false :
this.Filters_D[Index].Field.Type === 'integer' ? 0 :
this.Filters_D[Index].Field.Type === 'string' ? '' : undefined
this.Filters_D[Index].Field.Type === 'string' ? '' :
this.Filters_D[Index].Field.Type === $TYPES.Status ? $STATUS_VALUES[0] : undefined
},
// Returns the available conditions for the specified column name.
GetConditions(ColumnName)
{
if (ColumnName === 'status')
{
const ValidConditions = {}
Object.keys($CONDITIONS).forEach(Key => {
if (Key === 'Eq' || Key === 'NotEq')
ValidConditions[Key] = $CONDITIONS[Key]
})
return Object.values(ValidConditions)
}
else return Object.values($CONDITIONS)
},
},
created()
{
window.addEventListener('resize', this.InitTrHeight)
// --- Expose Template Constants ---
this.$CONDITIONS = $CONDITIONS
this.$TYPES = $TYPES
this.$STATUS_VALUES = $STATUS_VALUES
},
mounted()
@ -317,4 +411,28 @@ export default {
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>
</style>
<!--
DOCUMENTATION:
INTRODUCTION:
The responsibility of this component is to let the user create a custom array of
objects, and return it through an event.
LIST OF COLUMNS:
To specify the list of columns you can filter, modify the RemainingTableColumns_D
reactive variable in the data() section.
ADDING NEW COLUMN TYPES:
To add new column types, you must perform two steps:
1. first you have to go to "const $TYPES" at the top of the script tag to
add the new type.
2. then you should go to $COLUMN_VALUES in the template section, and add
the appropriate UI component.
-->

View File

@ -0,0 +1,119 @@
<template>
<b-modal id="modal-show-user" v-bind:centered="true" size="lg">
<!-- Header -->
<template v-slot:modal-header="{ close }">
<h2 class="text-primary m-0">User's Information</h2>
<img style="cursor: pointer;" src="@/assets/svg/cross.svg" alt="close-icon" v-on:click="close()">
</template>
<!-- Body -->
<template v-if="data" v-slot:default class="d-flex flex-column" style="">
<b-row>
<b-col>
<img
v-if="data.firm_avatar_url"
v-bind:src="data.firm_avatar_url"
alt="avatar"
class="rounded-circle mb-2"
style="object-fit: contain; width: 6rem; height: 6rem;"
>
<img
v-else
src="@/assets/images/default-profile.jpg"
alt="avatar"
class="rounded-circle mb-2"
style="object-fit: contain; width: 6rem; height: 6rem;"
>
</b-col>
</b-row>
<b-row style="row-gap: 1rem;">
<b-col md="6" xl="4">
<span class="info-key">First Name:</span>
<span class="info-value">{{ data.first_name }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Last Name:</span>
<span class="info-value">{{ data.last_name }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Email:</span>
<span class="info-value">{{ data.email_address }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Phone Number:</span>
<span class="info-value">{{ data.phone_number }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Total Uploaded Size:</span>
<span class="info-value">{{ FormatBytes(data.total_uploaded_size) }}</span>
</b-col>
<b-col md="6" xl="4">
<span class="info-key">Is Firm:</span>
<span class="info-value">
<svg v-if="data.is_main" style="width: 24px; height: 24px;" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
<svg v-else style="width: 24px; height: 24px;" viewBox="0 0 24 24">
<path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
</span>
</b-col>
</b-row>
</template>
<!-- Footer -->
<template v-slot:modal-footer="{ hide }">
<a class="text-primary" v-on:click="hide()">Close</a>
</template>
</b-modal>
</template>
<script>
import { BForm, BButton, BRow, BCol } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import { SeparateNumberByThousands, FormatBytes } from '@/modules/core'
export default {
name: 'AddUserModalComponent',
props: {
data: {
type: Object,
required: false,
},
},
components: {
BForm,
BButton,
BRow,
BCol,
},
directives: {
Ripple,
},
data() {
return {
}
},
methods: {
FormatBytes(Bytes)
{
return FormatBytes(Bytes)
},
},
}
</script>
<style scoped lang="scss">
.info-key {
font-weight: 500;
font-size: 16px;
}
.info-value {
margin-inline-start: 0.5rem;
}
</style>

View File

@ -7,10 +7,11 @@
<!-- </div>-->
<ASpin :spinning="loading" :indicator="indicator">
<b-row class="mb-4" style="row-gap: 0.75rem;">
<b-col md="6">
<b-col md="3">
<b-input-group v-if="search">
<b-form-input
id="search"
v-model="query.te.s.text"
v-bind:placeholder="options.placeholder"
>
</b-form-input>
@ -21,7 +22,20 @@
</b-input-group-append>
</b-input-group>
</b-col>
<b-col md="6" class="d-flex align-items-center">
<!-- Filters -->
<b-col md="1">
<b-button
class="mb-1"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
v-b-toggle.user-filters--sidebar
>
Filters
</b-button>
</b-col>
<b-col md="8" class="d-flex align-items-center">
<a class="left-table-btn" v-on:click="fullScreen">
<feather-icon
v-bind:icon="getFullIcon"
@ -71,16 +85,15 @@
</b-row>
<a-table
@change="change"
:indentSize="5"
:pagination="false"
ref="tbl"
:locale="locale"
:columns="visibleCols"
:data-source="data"
:row-selection="options.is_row_selection ? { selectedRowKeys: selectedRowKeys, onChange: onChange } : null"
>
<a-table @change="change"
table-layout="auto"
:indentSize="5"
:pagination="false"
ref="tbl"
:locale="locale"
:columns="visibleCols"
:data-source="data"
:row-selection="options.is_row_selection ? { selectedRowKeys: selectedRowKeys, onChange: onChange } : null">
<template :slot="item" slot-scope="name, record, index" v-for="(item,ind) in customRender">
<slot :name="item" :text="name" :record="record" :index="index"></slot>
</template>
@ -101,12 +114,12 @@
v-if="is_pagenation"
show-size-changer
:pageSizeOptions="['5','10','20']"
:page-size.sync="query.limit"
:page-size.sync="query.te.p.s"
@change="changePage"
@showSizeChange="changePage"
:default-current="1"
:total="query.total"
v-model.sync="query.page"
v-model.sync="query.te.p.c"
>
<template slot="buildOptionText" slot-scope="props">
<div dir="rtl">
@ -148,6 +161,8 @@
</a-table>
</ASpin>
</ACard>
<UserFiltersRepeatingForm v-on:submit="applyFilters($event)"/>
</div>
</template>
@ -161,7 +176,10 @@ import Draggable from 'vuedraggable'
import axios from "../../axios";
import {openFullscreen, closeFullscreen} from "@/plugins/fullscreen";
import NumberXTableFilter from "./NumberXTableFilter";
import { BRow, BCol, BInputGroup, BInputGroupAppend, BFormInput, BFormCheckbox, BButton } from 'bootstrap-vue'
import { BRow, BCol, BInputGroup, BInputGroupAppend, BFormInput, BFormCheckbox, BButton, VBToggle } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import UserFiltersRepeatingForm from '@/components/ui/UserFiltersRepeatingForm.vue'
const list = [
{
@ -192,6 +210,12 @@ export default {
BFormInput,
BFormCheckbox,
BButton,
UserFiltersRepeatingForm,
},
directives: {
'b-toggle': VBToggle,
Ripple,
},
props: {
@ -199,6 +223,7 @@ export default {
is_pagenation: { type: Boolean, default: true },
method: { type: String, default: "post" },
search: { type: Boolean, default: true },
ascending: { type: Boolean, default: true },
options: {
type: Object,
default: () => ({
@ -217,13 +242,15 @@ export default {
testTheme:[],
list:[],
query: {
search: undefined,
total: 0,
limit: 10,
page: 1,
query: [],
// search: undefined,
// total: 0,
// limit: 10,
// page: 1,
te: {
f: [],
p: { c: 1, s: 10 },
s: { cols: null, text: null },
o: { field: '', direction: '' },
}
},
locale: {
@ -270,13 +297,13 @@ export default {
changePage(item) {
// this.query.page = item;
this.query.te.p.c = item + 1
this.query.te.p.c = item
this.fetch();
this.$emit('changePage')
},
fullScreen() {
console.log("this.$refs.tblCard.$el ==> ", this.$refs.tblCard.$el);
// console.log("this.$refs.tblCard.$el ==> ", this.$refs.tblCard.$el);
if (this.full) {
closeFullscreen()
this.full = false;
@ -287,14 +314,19 @@ export default {
},
requestData() {
return this.query;
this.query.te.o.field = 'id'
if (this.ascending) this.query.te.o.direction = 'asc'
else this.query.te.o.direction = 'desc'
return this.query
},
refresh ($event)
{
this.$emit('refresh')
this.query.query = []
this.query.search = undefined
this.query.te.s.text = ""
this.query.te.s.cols = []
this.query.te.p.c = 1
this.fetch()
},
@ -303,14 +335,16 @@ export default {
try {
this.loading = true;
if(this.method == "post"){
const {data: {rows, query}} = await axios.post(this.model.url, {
const {data: {rows, te, total, page}} = await axios.post(this.model.url, {
...this.requestData()
});
this.data = rows;
// this.query.page = query.page
// this.query.limit = query.limit;
// this.query.total = query.total;
this.query.total = total;
// this.query.max_page = query.max_page;
this.query.te = te
// this.query.te = Object.assign(te, { p: { c: te.p.c - 1 } })
this.$emit('rows',rows)
}else{
const {data} = await axios.get(this.model.url);
@ -331,9 +365,13 @@ export default {
},
onSearch(value) {
this.query.page = 1
this.query.search = value
this.query.query = [];
// Old API
// TODO: If not needed, just remove this.
// this.query.page = 1
// this.query.search = value
this.query.te.p.c = 1
this.query.te.s.cols = ["id"]
if (value) {
this.fetch();
}
@ -341,7 +379,14 @@ export default {
showColSelector() {
this.colSelector = !this.colSelector;
}
},
async applyFilters(filters)
{
console.log()
this.query.te.fi = filters
this.fetch()
},
},
computed: {

View File

@ -24,7 +24,8 @@
"plan_id": "Plan",
"priority": "Priority",
"contract_id": "Contract Id",
"value": ""
"value": "",
"email": "Email"
},
"message": {
"title": "Card Title",

View File

@ -12,73 +12,73 @@ export default [
title: 'Users List',
route: 'users-list',
},
{
title: 'Add User',
route: 'users-add',
},
{
title: 'User Reports',
route: 'users-reports',
},
{
title: 'Deleted Users List',
route: 'users-deleted',
},
],
},
{
title: 'Admins',
icon: 'Edit2Icon',
children: [
{
title: 'Add Admin',
route: 'admins-add',
},
],
},
{
title : 'Permissions',
icon : 'LockIcon' ,
route : 'permissions',
},
{
title : 'My Activities',
icon : 'ActivityIcon' ,
route : 'activities' ,
},
{
title: 'Financial',
icon: 'DollarSignIcon',
children: [
{
title: 'Financial List',
route: 'financial-list',
},
{
title: 'Financial Reports',
route: 'financial-reports',
},
],
},
{
title: 'Refunds',
icon: 'RefreshCcwIcon',
children: [
{
title: 'Refunds List',
route: 'refunds-list',
},
{
title: 'Add Refund',
route: 'refunds-add',
},
{
title: 'Refund Reports',
route: 'refunds-reports',
},
// {
// title: 'Add User',
// route: 'users-add',
// },
// {
// title: 'User Reports',
// route: 'users-reports',
// },
// {
// title: 'Deleted Users List',
// route: 'users-deleted',
// },
],
},
// {
// title: 'Admins',
// icon: 'Edit2Icon',
// children: [
// {
// title: 'Add Admin',
// route: 'admins-add',
// },
// ],
// },
// {
// title : 'Permissions',
// icon : 'LockIcon' ,
// route : 'permissions',
// },
// {
// title : 'My Activities',
// icon : 'ActivityIcon' ,
// route : 'activities' ,
// },
// {
// title: 'Financial',
// icon: 'DollarSignIcon',
// children: [
// {
// title: 'Financial List',
// route: 'financial-list',
// },
// {
// title: 'Financial Reports',
// route: 'financial-reports',
// },
// ],
// },
// {
// title: 'Refunds',
// icon: 'RefreshCcwIcon',
// children: [
// {
// title: 'Refunds List',
// route: 'refunds-list',
// },
// {
// title: 'Add Refund',
// route: 'refunds-add',
// },
// {
// title: 'Refund Reports',
// route: 'refunds-reports',
// },
// ],
// },
// {
// title: 'Lawyers',
// icon: 'FeatherIcon',
// children: [
@ -146,22 +146,26 @@ export default [
title: 'Subscription Rules',
route: 'subscription-rules',
},
// This was so old (Najjar-era) that was commented out. No need
// anymore. Kept for the UI example.
// {
// title: 'Subscription Subscriptions',
// route: 'subscription-subscriptions',
// },
{
title: 'Subscription Transactions',
route: 'subscription-transactions',
},
{
title: 'Subscription Payments',
route: 'subscription-payments',
},
{
title: 'Subscription Contract Users',
route: 'subscription-contractusers',
},
// {
// title: 'Subscription Transactions',
// route: 'subscription-transactions',
// },
// {
// title: 'Subscription Payments',
// route: 'subscription-payments',
// },
// {
// title: 'Subscription Contract Users',
// route: 'subscription-contractusers',
// },
],
},
]

View File

@ -0,0 +1,353 @@
<!--
DOCUMENTATION:
Description:
A modal that prompts for a user/lawyer/law firm's information.
How to use:
First, import the component, put it in the template section, and then use
VBModal bootstrap-vue component and pass it modal-add-use argument to open the
modal.
Example:
<b-button v-b-modal:modal-add-user>Add New User</b-button>
Props:
Name Type Description
------ ------- --------------------------------------------------------------
data Object The JSON to initialize the user's data with
Events
Name Parameters Description
-------- ----------- ----------------------------------------------------------
-->
<template>
<b-modal id="modal-edit-user" v-bind:centered="true" size="lg" v-on:shown="OnModalShow()">
<!-- Header -->
<template v-slot:modal-header="{ close }">
<h2 class="text-primary m-0">Edit User</h2>
<img style="cursor: pointer;" src="@/assets/svg/cross.svg" alt="close-icon" v-on:click="close()">
</template>
<!-- Body -->
<template v-slot:default>
<validation-observer ref="editUserForm">
<b-form class="d-flex flex-column">
<b-row>
<b-col md="6">
<b-form-group label="Payed Amount" label-for="txtPayedAmount">
<b-form-input id="txtPayedAmount" v-model="Payload_D.payed_amount" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="Payable Amount" label-for="txtPayableAmount">
<b-form-input id="txtPayableAmount" v-model="Payload_D.payable_amount" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="Total Price" label-for="txtTotalPrice">
<b-form-input id="txtTotalPrice" v-model="Payload_D.total_price" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="Installments Count" label-for="txtInstallmentsCount">
<b-form-input id="txtInstallmentsCount" v-model="Payload_D.installments_count" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="User Usage" label-for="txtUserUsage">
<b-form-input id="txtUserUsage" v-model="Payload_D.user_usage" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="User Limit" label-for="txtUserLimit">
<b-form-input id="txtUserLimit" v-model="Payload_D.user_limit" />
</b-form-group>
</b-col>
<!-- NOTE: Doesn't makes sense to edit storage usage. It specifies how much of the
storage given to the user was used by him/her. Manually editing it introduces buggy
behavior to the program. -->
<!-- <b-col md="6">
<b-form-group label="Storage Usage" label-for="txtStorageUsage">
<b-form-input id="txtStorageUsage" v-model="Payload_D.storage_usage" />
</b-form-group>
</b-col> -->
<b-col md="6">
<b-form-group label="Storage Limit (MB)" label-for="Storage Limit">
<validation-provider
v-slot:default="{ errors }"
name="Storage Limit"
v-bind:rules="{ required, regex: /^\d+(\.\d+)?$/ }">
<b-form-input
id="Storage Limit"
v-model="Payload_D.storage_limit"
v-bind:state="errors.length > 0 ? false : null" />
<small class="text-danger">{{ errors[0] }}</small>
</validation-provider>
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="User Type" label-for="dpdUserType">
<v-select id="dpdUserType"
v-bind:options="TypeOptions_D"
label="title"
v-model="Payload_D.type"
v-bind:clearable="false" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="Status" label-for="dpdStatus">
<v-select id="dpdStatus"
v-bind:options="StatusOptions_D"
label="title"
v-model="Payload_D.status"
v-bind:clearable="false" />
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="Start Date" label-for="dpStartDate">
<b-form-datepicker
id="dpStartDate"
v-model="Payload_D.from_date"
v-bind:date-format-options="{ day: 'numeric', month: 'long', year: 'numeric' }"
today-button
close-button
label-help=""
nav-button-variant="primary"
/>
</b-form-group>
</b-col>
<b-col md="6">
<b-form-group label="End Date" label-for="dpToDate">
<b-form-datepicker
id="dpToDate"
v-model="Payload_D.to_date"
v-bind:date-format-options="{ day: 'numeric', month: 'long', year: 'numeric' }"
today-button
close-button
label-help=""
nav-button-variant="primary"
/>
</b-form-group>
</b-col>
<!--
<b-col class="my-1" md="5" lg="4">
<b-form-group label="Gender">
<div class="d-flex" style="column-gap: 1rem;">
<b-form-radio value="0" name="gender">Male</b-form-radio>
<b-form-radio value="1" name="gender">Female</b-form-radio>
</div>
</b-form-group>
</b-col>
<b-col class="my-1" md="5" lg="4">
<b-form-group label="Martial Status">
<div class="d-flex" style="column-gap: 1rem;">
<b-form-radio value="0" name="martial-status">Married</b-form-radio>
<b-form-radio value="1" name="martial-status">Single</b-form-radio>
</div>
</b-form-group>
</b-col>
<b-col class="my-1" md="5" lg="4">
<b-form-group label="Status">
<div class="d-flex" style="column-gap: 1rem;">
<b-form-radio value="0" name="status">Active</b-form-radio>
<b-form-radio value="1" name="status">Inactive</b-form-radio>
</div>
</b-form-group>
</b-col>
<b-col class="add-user-form--bottom-row" md="12">
<b-form-group id="phone-number" label="Phone Number:">
<div style="display: flex; align-items: center; column-gap: 1rem;">
<b-form-input type="number" v-bind:max="3" />
<span style="min-width: 0.83rem;">- -</span>
<b-form-input type="number" v-bind:max="4" />
<b-form-input type="number" v-bind:max="4" />
<b-form-input type="number" v-bind:max="4" />
</div>
</b-form-group>
<b-form-group v-if="type === 'user'" id="password" label="Password:">
<b-form-input />
</b-form-group>
<b-form-group id="address" label="Address:">
<b-form-textarea rows="4" no-resize style="margin-top: 0.2rem;" />
</b-form-group>
</b-col> -->
</b-row>
</b-form>
</validation-observer>
</template>
<!-- Footer -->
<template v-slot:modal-footer="{ hide }">
<b-button class="mr-1"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
v-on:click="OnSubmitButtonClicked(hide)">
Update
</b-button>
or
<a class="text-primary" v-on:click="hide()">Cancel</a>
</template>
</b-modal>
</template>
<script>
import { ValidationProvider, ValidationObserver } from 'vee-validate'
import { BForm, BButton, BRow, BCol, BFormGroup, BFormInput, BFormDatepicker, BFormRadio, BFormTextarea } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import vSelect from 'vue-select'
import axios from '@/axios'
import { regex, required } from '@validations'
export default {
name: 'AddUserModalComponent',
props: {
data: {
type: Object,
required: false,
},
},
components: {
ValidationProvider,
ValidationObserver,
BForm,
BButton,
BRow,
BCol,
BFormGroup,
BFormInput,
BFormDatepicker,
BFormRadio,
BFormTextarea,
vSelect,
},
directives: {
Ripple,
},
data() {
return {
Payload_D: {
payed_amount: '',
installments_count: '',
from_date: '',
user_limit: '',
status: '',
storage_limit: '',
to_date: '',
type: '',
// storage_usage: '', // NOTE: While possible, don't edit this value.
total_price: '',
allowed_delay_days: '',
user_usage: '',
discounted_price: '',
payable_amount: '',
},
UserId_D: -1,
StatusOptions_D: [
{ value: 1, title: 'Created' },
{ value: 2, title: 'Active' },
{ value: 3, title: 'Deactivate' },
{ value: 4, title: 'Expired' },
],
TypeOptions_D: [
{ value: 1, title: 'From Plans' },
{ value: 2, title: 'Manual' },
{ value: 3, title: 'Free' },
],
// Validation Rules
regex,
required
}
},
methods: {
OnSubmitButtonClicked(Hide)
{
this.$refs.editUserForm.validate().then(success => {
if (success)
{
this.$store.commit('app/toggle_full_page_overlay')
const tmp_payload = Object.assign({}, this.Payload_D)
tmp_payload.type = tmp_payload.type.value
tmp_payload.status = tmp_payload.status.value
tmp_payload.storage_limit = tmp_payload.storage_limit * 1024 * 1024 // Convert from MegaByte to Byte
axios.put(`admin/api/v1/contract/${this.UserId_D}`, tmp_payload)
.then(response => {
this.$bvToast.toast('User updated successfully.', {
title: 'Success',
autoHideDelay: 5000,
appendToast: false,
variant: 'success',
})
Hide()
this.$emit('updated')
})
.catch(error => {
this.$bvToast.toast('There was an error while updating the user. Please try again later.', {
title: 'Failure',
autoHideDelay: 5000,
appendToast: false,
variant: 'danger',
})
})
.finally(() => {
this.$store.commit('app/toggle_full_page_overlay')
})
}
})
},
OnModalShow()
{
this.UserId_D = this.data.id
this.Payload_D.payed_amount = this.data.payed_amount
this.Payload_D.installments_count = this.data.installments_count
this.Payload_D.from_date = this.data.from_date
this.Payload_D.user_limit = this.data.user_limit
this.Payload_D.storage_limit = (this.data.storage_limit / 1024 / 1024).toFixed(4) // Convert from Byte to MegaByte
this.Payload_D.to_date = this.data.to_date
this.Payload_D.storage_usage = this.data.storage_usage
this.Payload_D.total_price = this.data.total_price
this.Payload_D.allowed_delay_days = this.data.allowed_delay_days
this.Payload_D.user_usage = this.data.user_usage
this.Payload_D.discounted_price = this.data.discounted_price
this.Payload_D.payable_amount = this.data.payable_amount
this.Payload_D.type = {
value: this.data.type,
title: this.TypeOptions_D
.reduce((Total, CurrentValue) => (CurrentValue.value === this.data.type) ? CurrentValue.title : Total, '')
}
this.Payload_D.status = {
value: this.data.status,
title: this.StatusOptions_D
.reduce((Total, CurrentValue) => (CurrentValue.value === this.data.status) ? CurrentValue.title : Total, '')
}
}
},
}
</script>
<style scoped lang="scss">
</style>

View File

@ -0,0 +1,248 @@
<!--
Documentation:
Description:
A modal that sends the an specified invoice to one or more emails.
How to use:
First, import the component, put it in the template section, and then use
VBModal bootstrap-vue component and pass it modal-add-use argument to open the
modal.
Example:
Opening the modal via HTML:
<b-button v-b-modal:modal-send-invoice>Add New User</b-button>
Or opening the modal via code:
this.$bvModal.show('modal-send-invoice')
Props:
Name Type Description
------ ------- --------------------------------------------------------------
user_id Number The 'id' of the user whose invoice will be sent to the emails
-->
<template>
<b-modal id="modal-send-invoice"
v-model="ModalVisible_D"
v-bind:centered="true"
size="lg"
v-on:shown="OnModalShown()">
<!-- Header -->
<template v-slot:modal-header="{ close }">
<h2 class="text-primary m-0">Send Invoice</h2>
<img style="cursor: pointer;" src="@/assets/svg/cross.svg" alt="close-icon" v-on:click="close()">
</template>
<!-- Body -->
<template v-slot:default>
<transition>
<b-form ref="emails"
id="emails-repeater-form"
class="d-flex flex-column"
v-bind:style="{ height: trHeight }">
<b-row ref="row"
v-for="(Email, Index) in Emails_D"
v-bind:key="Index">
<b-col md="9">
<b-form-group label="Email" label-for="txtFirstName">
<b-form-input v-model="Email.Value" />
</b-form-group>
</b-col>
<b-col lg="3" class="mb-1">
<b-form-group label=" ">
<b-button v-ripple.400="'rgba(234, 84, 85, 0.15)'"
variant="outline-danger"
class="mt-md-1"
v-on:click="Remove(Index)"
>
<feather-icon icon="XIcon" class="mr-25" />
<span>Delete</span>
</b-button>
</b-form-group>
</b-col>
</b-row>
</b-form>
</transition>
</template>
<!-- Footer -->
<template v-slot:modal-footer="{ hide }">
<b-button variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
style="height: 38px;"
v-on:click="Clear()">
Clear All Emails
</b-button>
<b-button class="mr-4"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
style="height: 38px;"
v-on:click="Repeat()">
Add Email
</b-button>
<b-button
class="mr-1"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
v-on:click="OnSubmitButtonClicked(hide)"
>
Send Invoice
</b-button>
or
<a class="text-primary" v-on:click="hide()">Cancel</a>
</template>
</b-modal>
</template>
<script>
import { BForm, BButton, BRow, BCol, BFormGroup, BFormInput } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import { heightTransition } from '@core/mixins/ui/transition'
import axios from '@/axios'
export default {
name: 'SendInvoiceModalComponent',
props: {
user_id: {
type: Number,
required: true,
},
},
components: {
BForm,
BButton,
BRow,
BCol,
BFormGroup,
BFormInput,
},
directives: {
Ripple,
},
mixins: [ heightTransition ],
data() {
return {
Emails_D: [{ Value: '' }],
// This exists so we can check whether the modal is opened or closed.
// Do not use to close or open the modal.
ModalVisible_D: false,
}
},
methods: {
// Initializes the height of the transition.
InitTrHeight()
{
if (this.ModalVisible_D)
{
this.trSetHeight(null)
this.$nextTick(() => {
this.trSetHeight(this.$refs.emails.scrollHeight)
})
}
},
// Adds a new row to the emails form repeater.
Repeat()
{
this.Emails_D.push({ Value: '' })
this.$nextTick(() => {
this.trAddHeight(this.$refs.row[0].offsetHeight)
})
},
// Deletes a row from the emails form repeater.
Remove(Index)
{
if (this.Emails_D.length === 1)
{
this.$bvToast.toast('There must be at least one email!', {
title: 'Error',
autoHideDelay: 5000,
appendToast: false,
variant: 'danger',
})
return
}
this.Emails_D.splice(Index, 1)
this.trTrimHeight(this.$refs.row[0].offsetHeight)
},
// Removes all emails.
Clear()
{
this.Emails_D = [{ Value: '' }]
this.trSetHeight('86.975')
},
OnSubmitButtonClicked(Hide)
{
this.$store.commit('app/toggle_full_page_overlay')
const data = { to_emails: this.Emails_D.map(item => item.Value) }
axios.post(`admin/api/v1/send-invoice/${this.user_id}`, data)
.then(response => {
this.$bvToast.toast('Invoice was sent successfully.', {
title: 'Success',
autoHideDelay: 5000,
appendToast: false,
variant: 'success',
})
Hide()
})
.catch(error => {
this.$bvToast.toast('There was an error while sending the invoice. Please try again later.', {
title: 'Failure',
autoHideDelay: 5000,
appendToast: false,
variant: 'danger',
})
})
.finally(() => {
this.$store.commit('app/toggle_full_page_overlay')
})
},
OnModalShown()
{
this.Clear()
},
},
created()
{
window.addEventListener('resize', this.InitTrHeight)
},
destroyed()
{
window.removeEventListener('resize', this.InitTrHeight)
},
}
</script>
<style scoped lang="scss">
#emails-repeater-form {
transition: 0.35s height;
}
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to {
opacity: 0;
}
</style>

View File

@ -0,0 +1,111 @@
<template>
<b-modal id="modal-show-subusers"
v-bind:centered="true"
size="lg"
v-on:shown="OnShown()"
v-on:hide="OnHide()">
<!-- Header -->
<template v-slot:modal-header="{ close }">
<h2 class="text-primary m-0">{{ data.OwnerName }}'s Sub-users</h2>
<img style="cursor: pointer;" src="@/assets/svg/cross.svg" alt="close-icon" v-on:click="close()">
</template>
<!-- Body -->
<template v-slot:default>
<div v-if="Loading_D === 'pending'" class="text-center my-4">
<b-spinner variant="secondary" label="Spinning"></b-spinner>
</div>
<div v-else-if="Loading_D === 'success' && Data_D.length > 0" style="overflow-x: auto;">
<b-table striped hover v-bind:items="FilteredData()" class="mt-1"></b-table>
</div>
<b-alert v-else-if="Loading_D === 'success' && Data_D.length === 0" show variant="secondary" class="mt-1 py-1 px-2">
This account currently has no subusers.
</b-alert>
<b-alert v-else-if="Loading_D === 'failure'" show variant="danger" class="mt-1 py-1 px-2">
There was an error with the internet connection. Please try again.
</b-alert>
</template>
<!-- Footer -->
<template v-slot:modal-footer="{ hide }">
<a class="text-primary" v-on:click="hide()">Close</a>
</template>
</b-modal>
</template>
<script>
import { BForm, BButton, BRow, BCol, BSpinner, BTable, BAlert } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import axios from '@/axios'
export default {
name: 'ShowSubusersModalComponent',
props: {
data: {
required: true,
},
},
data() {
return {
Loading_D: 'pending',
Data_D: [],
}
},
components: {
BForm,
BButton,
BRow,
BCol,
BSpinner,
BTable,
BAlert,
},
directives: {
Ripple,
},
methods: {
async OnShown()
{
try
{
const { data } = await axios.get(`admin/api/v1/subuser/${this.data.OwnerId}`)
this.Data_D = data.users
this.Loading_D = 'success'
}
catch (e)
{
this.Loading_D = 'failure'
}
},
OnHide()
{
this.Data_D = []
this.Loading_D = 'pending'
},
FilteredData()
{
var index = 0
return this.Data_D.map(item => {
index = index + 1
return {
id: index,
name: `${item.first_name} ${item.last_name}`,
email_address: item.email_address,
phone_number: item.phone_number,
}
})
},
},
}
</script>
<style scoped lang="scss">
</style>

View File

@ -1,21 +1,23 @@
<template>
<!-- <div v-bind:class="{ 'page--main': Users_D.length === 0 }"> -->
<div class="page--main">
<FullPageLoadingComponent v-bind:show="$store.state.app.FullPageOverlayVisible" />
<!----------------------------- Title ----------------------------->
<div class="d-flex justify-content-between">
<h3>Users List</h3>
<div v-if="Users_D.length > 0" class="sx2">
<!-- <div v-if="Users_D.length > 0" class="sx2">
<b-button variant="danger" v-ripple.400="'rgba(255, 255, 255, 0.15)'">Remove</b-button>
<b-button variant="primary" v-ripple.400="'rgba(255, 255, 255, 0.15)'" v-b-modal:modal-add-user>
Add
</b-button>
</div>
</div> -->
</div>
<!------------------------------ Body ----------------------------->
<b-card class="position-relative mt-1" style="min-height: 95%;">
<!-- No Users Exist -->
<div id="no-users--container" v-if="Users_D.length === 0" class="d-flex flex-column justify-content-center align-items-center">
<!-- <div id="no-users--container" v-if="Users_D.length === 0" class="d-flex flex-column justify-content-center align-items-center">
<img src="@/assets/svg/hero.svg" alt="no-users-found">
<h4 class="mt-2" style="text-align: center;">You haven't created any users.</h4>
<p style="text-align: center;">Click the following button to create a new user.</p>
@ -26,92 +28,132 @@
>
Add User
</b-button>
</div>
</div> -->
<div v-else>
<!-- Filters -->
<b-button
class="mb-1"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
v-b-toggle.user-filters--sidebar
>
Filters
</b-button>
<div>
<!-- LIST OF USERS -->
<XTable ref="users"
v-bind:model="Model_D"
v-on:selectedRows="SelectedRows_D"
v-bind:options="TableOptions_D"
v-bind:ascending="false">
<span slot="avatar" slot-scope="text, record">
<img v-if="text.record.owner.firm_avatar_url"
v-bind:src="text.record.owner.firm_avatar_url"
alt="avatar"
class="rounded-circle"
style="object-fit: contain; width: 3rem; height: 3rem;">
<img v-else
src="@/assets/images/default-profile.jpg"
alt="avatar"
class="rounded-circle"
style="object-fit: contain; width: 3rem; height: 3rem;">
</span>
<!-- List of Users -->
<XTable
ref="users"
v-bind:model="Model_D"
v-on:selectedRows="SelectedRows_D"
v-bind:options="TableOptions_D"
>
<span slot="firm" slot-scope="text, record">
<svg v-if="text.record.is_main" style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
</svg>
<svg v-else style="width: 24px; height: 24px" viewBox="0 0 24 24">
<path fill="currentColor" d="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
</svg>
<span slot="name" slot-scope="text, record" class="d-flex justify-content-between">
<span style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 12rem;">
{{ `${text.record.owner.first_name} ${text.record.owner.last_name}` }}
</span>
<a class="text-primary"
style="margin-inline-start: 1rem;"
v-on:click="OpenModal('user', text.record.owner)">
More...
</a>
</span>
<span slot="charge" slot-scope="text, record">
<span v-if="text.record.subscription">${{ SeparateNumberByThousands(text.record.subscription.charge) }}</span>
<!-- <span v-if="text.record.subscription">$ {{ text.record.subscription.charge }}</span> -->
<span v-else>$0</span>
<span slot="status" slot-scope="text, record">
<b-alert v-if="text.record.status === 1" show variant="secondary" style="text-align: center; padding: 0.2rem 0.5rem;">Created</b-alert>
<b-alert v-else-if="text.record.status === 2" show variant="success" style="text-align: center; padding: 0.2rem 0.5rem;">Active</b-alert>
<b-alert v-else-if="text.record.status === 3" show variant="warning" style="text-align: center; padding: 0.2rem 0.5rem;">Inactive</b-alert>
<b-alert v-else-if="text.record.status === 4" show variant="danger" style="text-align: center; padding: 0.2rem 0.5rem;">Expired</b-alert>
</span>
<span slot="total_uploaded_size" slot-scope="text, record">
<span>{{ FormatBytes(text.record.total_uploaded_size) }}</span>
<span slot="plan" slot-scope="text, record">
<a v-if="text.record.plan"
class="text-primary"
v-on:click="OpenModal('plan', text.record.plan)">
{{ text.record.plan.title }}
</a>
<span v-else>-</span>
</span>
<span slot="total_firm_uploaded_size" slot-scope="text, record">
<span>{{ FormatBytes(text.record.total_firm_uploaded_size) }}</span>
<span slot="storage_usage" slot-scope="text, record">
<span style="white-space: nowrap;">{{ FormatBytes(text.record.storage_usage) }}</span>
</span>
<span slot="action" slot-scope="text, record">
<b-button
v-if="text.record.is_main"
variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
v-b-modal:modal-charge-user
v-on:click="IdOfUserToCharge_D = text.record.id"
>
Charge
<span slot="storage_limit" slot-scope="text, record">
<span style="white-space: nowrap;">{{ FormatBytes(text.record.storage_limit) }}</span>
</span>
<span slot="from_date" slot-scope="text, record">
<span style="white-space: nowrap;">{{ GetDate(text.record.from_date) }}</span>
</span>
<span slot="to_date" slot-scope="text, record">
<span style="white-space: nowrap;">{{ GetDate(text.record.to_date) }}</span>
</span>
<span slot="action" slot-scope="text, record" class="d-flex" style="column-gap: 1rem;">
<!-- NOTE: Manager currently decided to disable this temporarily, but will want
to enable it later on. -->
<b-button v-if="false" variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
style="white-space: nowrap;"
v-on:click="OpenModal('invoice', text.record.id)">
Send Invoice
</b-button>
<b-button variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
style="white-space: nowrap;"
v-on:click="OpenModal('edit', text.record)">
Edit
</b-button>
<b-button variant="primary"
v-ripple.400="'rgba(255, 255, 255, 0.15)'"
style="white-space: nowrap;"
v-on:click="ShowSubusers(text.record)">
Show Sub Users
</b-button>
</span>
</XTable>
</XTable> <!-- END LIST OF USERS -->
</div>
</b-card>
<UserFiltersRepeatingForm
v-on:submit="ApplyFilters($event)"
/>
<!-- class="mt-1" -->
<!-- v-if="Users_D.length > 0" -->
<AddUserModal type="user" v-on:submit="HandleNewUserAdded()" />
<ChargeUserModal v-bind:user-id="IdOfUserToCharge_D" v-bind:table-ref="$refs.users" v-on:submit="HandleUserCharged()" />
<!-- <AddUserModal type="user" v-on:submit="HandleNewUserAdded()" /> -->
<!-- <ChargeUserModal v-bind:user-id="IdOfUserToCharge_D" v-bind:table-ref="$refs.users" v-on:submit="HandleUserCharged()" /> -->
<UserInfoModal v-bind:data="SelectedRecord_D" />
<PlanInfoModal v-bind:data="SelectedRecord_D" />
<SendInvoiceModal v-bind:user_id="UserId_D" />
<EditUserModal v-bind:data="SelectedRecord_D" v-on:updated="$refs.users.fetch()" />
<ShowSubusersModal v-bind:data="SelectedRecord_D" />
</div>
</template>
<script>
import { BRow, BCol, BCard, BButton, VBModal, VBToggle } from 'bootstrap-vue'
import { BRow, BCol, BCard, BButton, BBadge, BAlert, VBModal, VBToggle } from 'bootstrap-vue'
import Ripple from 'vue-ripple-directive'
import TableComponent from '@/components/ui/TableComponent'
import AddUserModal from '@/components/ui/AddUserModal'
import ChargeUserModal from '@/components/ui/ChargeUserModal'
import * as TableCol from "./userTbl"
import XTable from "@/components/x-table/XTable"
import axios from '@/axios'
import { SeparateNumberByThousands, FormatBytes } from '@/modules/core'
import UserFiltersRepeatingForm from '@/components/ui/UserFiltersRepeatingForm.vue'
import FullPageLoadingComponent from '@/components/ui/FullPageLoadingComponent.vue'
// import CoreMixin from '@/mixins/index'
const InnerColumns = [
{ title: 'Emails', key: 'Emails', scopedSlots: { customRender: 'emails' } },
{ title: 'Websites', key: 'Websites', scopedSlots: { customRender: 'websites' } },
{ title: 'Phones', key: 'Phones', scopedSlots: { customRender: 'phones' } },
]
// Modals
import AddUserModal from '@/components/ui/AddUserModal'
import ChargeUserModal from '@/components/ui/ChargeUserModal'
import UserInfoModal from '@/components/ui/UserInfoModal'
import PlanInfoModal from '@/components/ui/PlanInfoModal'
import SendInvoiceModal from './Components/SendInvoiceModal'
import EditUserModal from './Components/EditUserComponent'
import ShowSubusersModal from './Components/ShowSubusersModal'
export default {
name: 'UsersListView',
@ -121,11 +163,18 @@ export default {
BCol,
BCard,
BButton,
BBadge,
BAlert,
TableComponent,
AddUserModal,
ChargeUserModal,
UserInfoModal,
PlanInfoModal,
SendInvoiceModal,
EditUserModal,
ShowSubusersModal,
XTable,
UserFiltersRepeatingForm,
FullPageLoadingComponent,
},
directives: {
@ -140,8 +189,8 @@ export default {
data() {
return {
Users_D: [ 1 ],
IdOfUserToCharge_D: -1, // This is the id of the user that we want to charge his/her account
Users_D: [ ],
UserId_D: -1,
Query_D: {
IsMain: false,
@ -159,6 +208,8 @@ export default {
is_row_selection: true,
is_load_req: true,
},
SelectedRecord_D: undefined,
}
},
@ -172,6 +223,11 @@ export default {
{
return FormatBytes(Bytes)
},
GetDate(DateTimeString)
{
return new Date(DateTimeString).toDateString()
},
HandleNewUserAdded()
{
@ -188,10 +244,38 @@ export default {
// ]
},
ApplyFilters(Filters)
OpenModal(ModalType, Data)
{
this.$refs.users.query.query = Filters
this.$refs.users.fetch()
if (ModalType === 'user')
{
this.SelectedRecord_D = Data
this.$bvModal.show('modal-show-user')
}
else if (ModalType === 'plan')
{
this.SelectedRecord_D = Data
this.$bvModal.show('modal-show-plan')
}
else if (ModalType === 'invoice')
{
this.UserId_D = Data
this.$bvModal.show('modal-send-invoice')
}
else if (ModalType === 'edit')
{
this.SelectedRecord_D = Data
this.$bvModal.show('modal-edit-user')
}
else throw new Error('Incorrect Parameter: ModalType can be either \'user\', \'plan\' or \'invoice\'.')
},
async ShowSubusers(Data)
{
this.SelectedRecord_D = {
OwnerId: Data.owner_id,
OwnerName: `${Data.owner.first_name} ${Data.owner.last_name}`
}
this.$bvModal.show('modal-show-subusers')
},
},

View File

@ -1,14 +1,20 @@
import XTbl, { Xtc } from '@/components/x-table/index'
const tbl = new XTbl('administrator/user/query/', 'Users')
tbl.add(new Xtc('first_name', 'First Name'))
tbl.add(new Xtc('last_name', 'Last Name'))
tbl.add(new Xtc('email_address', 'Email'))
tbl.add(new Xtc('is_main', 'Firm').renderSlot('firm'))
tbl.add(new Xtc('subscription', 'Charge').renderSlot('charge')) // subscription.charge
tbl.add(new Xtc('total_uploaded_size', 'Uploaded Size').renderSlot('total_uploaded_size'))
tbl.add(new Xtc('total_firm_uploaded_size', 'Firm Uploaded Size').renderSlot('total_firm_uploaded_size'))
const tbl = new XTbl('admin/api/v1/contract/', 'Users')
tbl.add(new Xtc('avatar', 'Avatar').noSort().renderSlot('avatar'))
tbl.add(new Xtc('name', 'Name').noSort().renderSlot('name'))
tbl.add(new Xtc('status', 'Status').noSort().renderSlot('status'))
tbl.add(new Xtc('user_usage', 'User Usage').noSort())
tbl.add(new Xtc('user_limit', 'User Limit').noSort())
tbl.add(new Xtc('payed_amount', 'Payed Amount').noSort())
tbl.add(new Xtc('payable_amount', 'Payable Amount').noSort())
tbl.add(new Xtc('total_price', 'Total Price').noSort())
tbl.add(new Xtc('installments_count', 'Installments Count').noSort())
tbl.add(new Xtc('storage_usage', 'Storage Usage').noSort().renderSlot('storage_usage'))
tbl.add(new Xtc('storage_limit', 'Storage Limit').noSort().renderSlot('storage_limit'))
tbl.add(new Xtc('from_date', 'Start Date').noSort().renderSlot('from_date'))
tbl.add(new Xtc('to_date', 'End Date').noSort().renderSlot('to_date'))
tbl.add(new Xtc('plan', 'Plan Info').noSort().renderSlot('plan'))
tbl.add(new Xtc('action', 'Action').noSort().renderSlot('action'))
export default tbl

View File

@ -230,8 +230,8 @@ export default {
data() {
return {
Payload_D: {
email: 'admin@irelex.com',
password: 'Iralex2021',
email: '',
password: '',
},
Loading_D: false,
ModalMessage_D : undefined,