Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl>
Reviewed-on: #45
This commit is contained in:
2025-10-04 11:40:52 -03:00
parent 6ddc48ec60
commit 742de657c5
815 changed files with 62089 additions and 3287 deletions

View File

@ -0,0 +1,207 @@
@extends('layout.base')
@section('page_content')
<div class="ui container">
<h2 class="ui header">Usuarios</h2>
<table class="ui table">
<thead>
<tr>
<th>Nombre</th>
<th class="right aligned">
<button class="ui mini green icon button" id="create-user-button">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
<tbody id="users">
@foreach($users as $user)
<tr data-user="{{ $user->id }}">
<td>{{ $user->name }}</td>
<td class="right aligned">
<button class="ui mini blue icon button edit" data-user="{{ $user->id }}">
<i class="edit icon"></i>
</button>
<button class="ui mini red icon button remove" data-user="{{ $user->id }}">
<i class="trash icon"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="ui modal" id="create-user-modal">
<i class="close icon"></i>
<div class="header">
Crear usuario
</div>
<div class="content">
<form class="ui form">
<div class="field">
<label>Nombre</label>
<input type="text" name="name" placeholder="Nombre">
</div>
<div class="field">
<label>Correo</label>
<input type="email" name="email" placeholder="Correo">
</div>
<div class="field">
<label>Contraseña</label>
<input type="password" name="password" placeholder="Contraseña">
</div>
<div class="field">
<label>Confirmar contraseña</label>
<input type="password" name="password_confirmation" placeholder="Confirmar contraseña">
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Crear
<i class="checkmark icon"></i>
</div>
</div>
</div>
<div class="ui modal" id="edit-user-modal">
<i class="close icon"></i>
<div class="header">
Editar usuario
</div>
<div class="content">
<form class="ui form">
<input type="hidden" name="id" />
<div class="field">
<label>Contraseña Antigua</label>
<input type="password" name="old_password" placeholder="Contraseña Antigua">
</div>
<div class="field">
<label>Force Change?</label>
<div class="ui checkbox">
<input type="checkbox" name="force" >
<label>Yes</label>
</div>
</div>
<div class="field">
<label>Contraseña</label>
<input type="password" name="password" placeholder="Contraseña">
</div>
<div class="field">
<label>Confirmar contraseña</label>
<input type="password" name="password_confirmation" placeholder="Confirmar contraseña">
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Guardar
<i class="checkmark icon"></i>
</div>
</div>
</div>
@endsection
@include('layout.body.scripts.cryptojs')
@push('page_scripts')
<script>
function encryptPassword(password) {
const passphrase = Math.floor(Math.random() * Date.now()).toString()
const encrypted = CryptoJS.AES.encrypt(password, passphrase)
return [passphrase, encrypted.toString()].join('')
}
$(document).ready(function () {
const $createUserModal = $('#create-user-modal')
$createUserModal.modal({
onApprove: function() {
const form = document.querySelector('#create-user-modal form')
const password = form.querySelector('[name="password"]').value
const password_confirmation = form.querySelector('[name="password_confirmation"]').value
if (password !== password_confirmation) {
alert('Las contraseñas no coinciden')
return
}
const url = '{{$urls->api}}/admin/users/add'
const method = 'post'
const body = new FormData(form)
body.set('password', encryptPassword(password))
fetchAPI(url, {method, body}).then(response => {
if (!response) {
return;
}
response.json().then(result => {
if (result.success) {
location.reload()
}
})
})
}
})
const $editUserModal = $('#edit-user-modal')
$editUserModal.modal({
onApprove: function() {
const form = document.querySelector('#edit-user-modal form')
const user_id = form.querySelector('[name="id"]').value
const old_password = form.querySelector('[name="old_password"]').value
const password = form.querySelector('[name="password"]').value
const password_confirmation = form.querySelector('[name="password_confirmation"]').value
if (password !== password_confirmation) {
alert('Las nuevas contraseñas no coinciden')
return
}
const url = `{{$urls->api}}/admin/user/${user_id}/edit`
const method = 'post'
const body = new FormData(form)
body.set('old_password', encryptPassword(old_password))
body.set('password', encryptPassword(password))
if (form.querySelector('[name="force"]').checked) {
body.set('force', 'true')
}
fetchAPI(url, {method, body}).then(response => {
if (!response) {
return;
}
response.json().then(result => {
if (result.success) {
location.reload()
}
})
})
}
})
document.getElementById('create-user-modal').addEventListener('submit', event => {
$createUserModal.modal('show')
})
document.querySelectorAll('.button.edit').forEach(button => {
button.addEventListener('click', clickEvent => {
const user_id = clickEvent.currentTarget.dataset.user
$editUserModal.find('input[name="id"]').val(user_id)
$editUserModal.modal('show')
})
})
document.querySelectorAll('.button.remove').forEach(button => {
button.addEventListener('click', clickEvent => {
const user_id = clickEvent.currentTarget.dataset.user
const url = `{{$urls->api}}/admin/user/${user_id}`
const method = 'delete'
fetchAPI(url, {method}).then(response => {
if (!response) {
return;
}
response.json().then(result => {
if (result.success) {
location.reload()
}
})
})
})
})
});
</script>
@endpush

View File

@ -10,5 +10,23 @@
<i class="hammer icon"></i>
Esta parte del sitio está en construcción.
</div>
@if (isset($exception))
<div class="ui warning message">
<i class="exclamation triangle icon"></i>
<div class="content">
<div class="header">Error</div>
<p>{{$message}}</p>
</div>
</div>
@endif
@if (isset($error))
<div class="ui error message">
<i class="exclamation triangle icon"></i>
<div class="content">
<div class="header">Error</div>
<p>{{$message}}</p>
</div>
</div>
@endif
</div>
@endsection

View File

@ -52,8 +52,7 @@
<th class="right aligned">Cargo</th>
<th class="right aligned">Abono</th>
<th class="right aligned">Saldo</th>
<th>Centro de Costo</th>
<th>Detalle</th>
<th class="right aligned"></th>
<th>Orden</th>
</tr>
</thead>
@ -115,10 +114,85 @@
</div>
</div>
</div>
<div class="ui modal" id="edit_modal" data-cartola="">
<div class="header">
Movimiento
<div class="meta">
<span id="edit_inmobiliaria"></span> |
<span id="edit_cuenta"></span> |
<span id="edit_fecha"></span> |
<span id="edit_glosa"></span> |
<span id="edit_cargo"></span> |
<span id="edit_abono"></span> |
<span id="edit_saldo"></span>
</div>
</div>
<div class="content">
<form class="ui form" id="edit_form">
<input type="hidden" name="movimiento_id" value="" />
<div class="equal width fields">
<div class="field">
<label>Centro de Costo</label>
<div class="ui search selection dropdown">
<input type="hidden" name="centro_costo_id" />
<i class="dropdown icon"></i>
<div class="default text">Centro de Costo</div>
<div class="menu">
@foreach($centrosCostos as $centro)
<div class="item" data-value="{{$centro->id}}">
{{$centro->id}} - {{$centro->descripcion}}
</div>
@endforeach
</div>
</div>
</div>
<div class="field">
<label>Categoría</label>
<div class="ui input">
<input type="text" name="categoria" />
</div>
</div>
<div class="field">
<label>Detalle</label>
<div class="ui input">
<input type="text" name="detalle" />
</div>
</div>
</div>
<div class="equal width fields">
<div class="field">
<label>RUT</label>
<div class="ui right labeled input">
<input type="text" name="rut" />
<div class="ui basic label">-<span id="digito"></span></div>
</div>
</div>
<div class="field">
<label>Nombres</label>
<div class="ui input">
<input type="text" name="nombres" />
</div>
</div>
<div class="field">
<label>Identificador</label>
<div class="ui input">
<input type="text" name="identificador" />
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui approve button">
Editar
</button>
</div>
</div>
@endsection
@include('layout.head.styles.datatables')
@include('layout.body.scripts.datatables')
@include('layout.body.scripts.rut')
@push('page_scripts')
<script>
@ -126,7 +200,8 @@
idx
inmobiliaria = {
rut: 0,
razon: ''
razon: '',
sigla: ''
}
cuenta = {
id: 0,
@ -187,46 +262,6 @@
}
}
}
update() {
return {
centro: (idx, centro_id) => {
const id = this.movimientos[idx].id
const url = '{{$urls->api}}/contabilidad/movimiento/' + id + '/detalles'
const body = new FormData()
body.set('centro_id', centro_id)
return fetchAPI(url, {method: 'post', body}).then(response => {
if (!response) {
return
}
response.json().then(json => {
if (!json.status) {
return
}
const dropdown = $(".dropdown[data-idx='" + idx + "']")
dropdown.dropdown('set selected', json.centro.id, true)
})
})
},
detalle: (idx, detalle) => {
const id = this.movimientos[idx].id
const url = '{{$urls->api}}/contabilidad/movimiento/' + id + '/detalles'
const body = new FormData()
body.set('detalle', detalle)
return fetchAPI(url, {method: 'post', body}).then(response => {
if (!response) {
return
}
response.json().then(json => {
if (!json.status) {
return
}
const input = $("input[data-idx='" + idx + "']")
input.val(json.detalle)
})
})
}
}
}
draw() {
return {
form: ($form, inmobiliarias, first = false) => {
@ -326,6 +361,10 @@
onChange: (value, text, $choice) => {
this.inmobiliaria.rut = value
this.inmobiliaria.razon = text
const inmobiliaria = diaria.data.inmobiliarias.find(inmobiliaria => inmobiliaria.rut === parseInt(value))
if (typeof inmobiliaria !== 'undefined') {
this.inmobiliaria.sigla = inmobiliaria.sigla
}
this.get().bancos(value).then(() => {
$cuentas_dropdown.dropdown('change values', this.bancos)
})
@ -380,74 +419,42 @@
cartola: ($tbody, dateFormatter, numberFormatter) => {
this.movimientos.forEach((row, idx) => {
$tbody.append(
$('<tr></tr>').append(
'<td>' + this.inmobiliaria.razon + '</td>' + "\n"
+ '<td>' + this.cuenta.descripcion + '</td>' + "\n"
+ '<td>' + dateFormatter.format(row.fecha) + '</td>' + "\n"
+ '<td>' + row.glosa + '</td>' + "\n"
+ '<td class="right aligned">' + (row.cargo === 0 ? '' : numberFormatter.format(row.cargo)) + '</td>' + "\n"
+ '<td class="right aligned">' + (row.abono === 0 ? '' : numberFormatter.format(row.abono)) + '</td>' + "\n"
+ '<td class="right aligned">' + (row.saldo === 0 ? '' : numberFormatter.format(row.saldo)) + '</td>' + "\n"
).append(
$('<td></td>').append(
this.draw().centroCosto(idx)
)
).append(
$('<td></td>').append(
this.draw().detalle(idx)
)
).append(
$('<td></td>').html(idx + 1)
)
[
'<tr>',
`<td>${this.inmobiliaria.sigla ?? this.inmobiliaria.razon}</td>`,
`<td>${this.cuenta.descripcion}</td>`,
`<td>${dateFormatter.format(row.fecha)}</td>`,
`<td>${row.glosa}</td>`,
`<td class="right aligned">${row.cargo === 0 ? '' : numberFormatter.format(row.cargo)}</td>`,
`<td class="right aligned">${row.abono === 0 ? '' : numberFormatter.format(row.abono)}</td>`,
`<td class="right aligned">${row.saldo === 0 ? '' : numberFormatter.format(row.saldo)}</td>`,
'<td class="center aligned">',
`<button class="ui icon button edit_movimiento" data-movimiento="${idx}">`,
'<i class="edit icon"></i>',
'</button>',
'</td>',
`<td>${(idx + 1).toString()}</td>`,
].join("\n")
)
})
Array.from(document.getElementsByClassName('edit_movimiento')).forEach(button => {
button.addEventListener('click', function(clickEvent) {
const idx = clickEvent.currentTarget.dataset['movimiento']
const movimiento = this.movimientos[idx]
const $modal = $('#edit_modal')
$modal.attr('data-cartola', this.idx)
$modal.find('#edit_inmobiliaria').html(this.inmobiliaria.razon)
$modal.find('#edit_cuenta').html(this.cuenta.descripcion)
$modal.find('#edit_fecha').html(dateFormatter.format(movimiento.fecha))
$modal.find('#edit_glosa').html(movimiento.glosa)
$modal.find('#edit_cargo').html(numberFormatter.format(movimiento.cargo))
$modal.find('#edit_abono').html(numberFormatter.format(movimiento.abono))
$modal.find('#edit_saldo').html(numberFormatter.format(movimiento.saldo))
$modal.find('input[name="movimiento_id"]').val(movimiento.id)
$modal.modal('show')
}.bind(this))
})
},
centroCosto: idx => {
const centros = JSON.parse('{!! json_encode($centrosCostos) !!}')
const menu = $('<div></div>').addClass('menu')
centros.forEach(centro => {
menu.append(
'<div class="item" data-value="' + centro.id + '">'
+ centro.id + ' - ' + centro.descripcion
+ '</div>'
)
})
const dropdown = $('<div></div>').addClass('ui search selection dropdown').attr('data-idx', idx).html(
'<input type="hidden" name="centro" />' + "\n" +
'<i class="dropdown icon"></i>' + "\n" +
'<div class="default text">Centro de Costo</div>' + "\n"
).append(menu)
dropdown.dropdown({
onChange: (value, text, $element) => {
const idx = $element.parent().parent().data('idx')
this.update().centro(idx, value)
}
})
if (this.movimientos[idx].centro !== '') {
const cid = centros.findIndex(centro => centro.descripcion === this.movimientos[idx].centro)
dropdown.dropdown('set selected', centros[cid].id, true)
}
return dropdown
},
detalle: idx => {
const detalle = document.createElement('input')
detalle.type = 'text'
detalle.name = 'detalle' + idx
detalle.placeholder = 'Detalle'
detalle.setAttribute('data-idx', idx)
const input = document.createElement('div')
input.className = 'ui input'
input.appendChild(detalle)
if (this.movimientos[idx].detalle !== '') {
detalle.value = this.movimientos[idx].detalle
}
detalle.addEventListener('blur', event => {
const idx = event.currentTarget.dataset['idx']
this.update().detalle(idx, event.currentTarget.value)
})
return $(input)
}
}
}
}
@ -460,8 +467,7 @@
'cargo',
'abono',
'saldo',
'centro',
'detalle',
'edit',
'orden'
];
@endphp
@ -471,17 +477,18 @@
inmobiliarias: JSON.parse('{!! json_encode($inmobiliarias) !!}'),
cartolasIdx: [],
cartolas: [],
movimientos: [],
},
dataTableConfig: {
order: [[{{array_search('orden', $columns)}}, 'asc']],
columnDefs: [
{
targets: [{{implode(',', array_keys(array_filter($columns, function($column) {return in_array($column, ['fecha', 'cargo', 'abono', 'saldo']);})))}}],
width: '{{round((1/(count($columns) + 3 - 1)) * 100,2)}}%'
targets: [{{implode(',', array_keys(array_filter($columns, function($column) {return in_array($column, ['fecha', 'cargo', 'abono', 'saldo', 'edit']);})))}}],
width: '{{round((1/(count($columns) + 2)) * 100, 2)}}%'
},
{
targets: [{{implode(',', array_keys(array_filter($columns, function($column) {return in_array($column, ['sociedad', 'cuenta', 'glosa', 'centro', 'detalle']);})))}}],
width: '{{round((1/(count($columns) + 3 - 1)) * 2 * 100, 2)}}%'
targets: [{{implode(',', array_keys(array_filter($columns, function($column) {return in_array($column, ['sociedad', 'cuenta', 'glosa']);})))}}],
width: '{{round((1/(count($columns) + 2)) * 2 * 100, 2)}}%'
},
{
targets: [{{array_search('orden', $columns)}}],
@ -605,8 +612,12 @@
cargo: row.cargo,
abono: row.abono,
saldo: row.saldo,
centro: row.detalles?.centro_costo.descripcion ?? '',
detalle: row.detalles?.detalle ?? ''
centro: row.detalles?.centro_costo?.id ?? '',
detalle: row.detalles?.detalle ?? '',
categoria: row.detalles?.categoria ?? '',
rut: row.detalles?.rut ?? '',
nombres: row.detalles?.nombres ?? '',
identificador: row.detalles?.identificador ?? ''
}
})
const ayer = new Date(this.data.cartolas[cartolaIdx].fecha.getTime())
@ -638,6 +649,51 @@
$(this.ids.table.base).DataTable(this.dataTableConfig)
this.cartolas().add()
$(this.ids.editModal).modal({
onApprove: $element => {
const idx = $(this.ids.editModal).data('cartola')
const id = $(this.ids.editModal).find('input[name="movimiento_id"]').val()
const body = new FormData($(this.ids.editModal).find('form')[0])
body.set('idx', idx)
if (body.has('rut')) {
body.set('rut', body.get('rut').replace(/\D/g, ''))
body.set('digito', Rut.digitoVerificador(body.get('rut')))
}
const url = '{{$urls->api}}/contabilidad/movimiento/' + id + '/detalles'
return fetchAPI(url, {method: 'post', body}).then(response => {
if (!response) {
return
}
response.json().then(json => {
if (!json.status || json.movimiento === null || json.movimiento.detalles === null) {
return
}
const idx = diaria.data.cartolas.findIndex(cartola => cartola.idx = json.input.idx)
const movimiento = diaria.data.cartolas[idx].movimientos.find(movimiento => movimiento.id === id)
movimiento.centro = json.movimiento.detalles.centro_costo.id
movimiento.categoria = json.movimiento.detalles.categoria
movimiento.detalle = json.movimiento.detalles.detalle
movimiento.rut = json.movimiento.detalles.rut
movimiento.digito = json.movimiento.detalles.digito
movimiento.nombres = json.movimiento.detalles.nombres
movimiento.identificador = json.movimiento.detalles.identificador
})
})
}
})
$(this.ids.editModal).find('.dropdown').dropdown()
$(this.ids.editModal).find('[name="rut"]').on('input', function(event) {
const rut = event.currentTarget.value
const rut_clean = rut.replace(/\D/g, '')
event.currentTarget.value = Rut.format(rut_clean)
document.getElementById('digito').innerHTML = Rut.digitoVerificador(rut_clean)
}.bind(this))
const rut = $(this.ids.editModal).find('[name="rut"]')
const rut_clean = rut.val().replace(/\D/g, '')
rut.val(Rut.format(rut_clean))
document.getElementById('digito').innerHTML = Rut.digitoVerificador(rut_clean)
}
}
const manual = {
@ -807,7 +863,8 @@
add: '#add_button',
process: '#process_button'
},
loader: '#loader'
loader: '#loader',
editModal: '#edit_modal'
})
manual.setup({
modal: '#manual_modal',

View File

@ -0,0 +1,394 @@
@extends('layout.base')
@section('page_content')
<div class="ui container">
<h1 class="ui header">
Importar Cartola Diaria
</h1>
<div class="ui grid">
<div class="right aligned sixteen wide column">
<button class="ui green icon button" id="add_button">
Agregar
<i class="plus icon"></i>
</button>
</div>
</div>
<form class="ui form" id="cartola_form">
</form>
<div class="ui two columns grid">
<div class="column">
<button class="ui icon button" id="process_button">
<i class="file excel icon"></i>
Procesar
</button>
</div>
<div class="right aligned column">
<div class="ui inline active loader" id="loader"></div>
</div>
</div>
</div>
<table class="ui celled table" id="movimientos" style="display: none;">
<thead>
<tr>
<th>Sigla</th>
<th>Fecha</th>
<th>Glosa</th>
<th class="right aligned">Cargo</th>
<th class="right aligned">Abono</th>
<th class="right aligned">Saldo</th>
<th class="center aligned">Centro de Costo</th>
<th>Categoría</th>
<th>Detalle</th>
<th>RUT</th>
<th>Nombres</th>
<th class="center aligned">Identificador</th>
<th></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
@endsection
@push('page_scripts')
<script>
class Cartola {
props
ids
constructor({index, ids}) {
this.props = {
index
}
this.ids = ids
}
get sociedad() {
return $(`#${this.ids.sociedad}${this.props.index}`).dropdown('get value')
}
get banco() {
return $(`#${this.ids.banco}${this.props.index}`).dropdown('get value')
}
get mes() {
return $(`#${this.ids.mes}${this.props.index}`).calendar('get date')
}
get file() {
return $(`#archivo${this.props.index}`)[0].files[0]
}
get data() {
return {
sociedad_rut: this.sociedad,
cuenta_id: this.banco,
mes: [this.mes.getFullYear(), this.mes.getMonth() + 1, 1].join('-'),
file: this.file,
index: this.props.index
}
}
draw({sociedades}) {
const output = [
`<div class="fields" data-idx="${this.props.index}">`,
'<div class="five wide field">',
'<label>Sociedad</label>',
`<div class="ui search selection dropdown" id="${this.ids.sociedad}${this.props.index}">`,
`<input type="hidden" name="sociedad_rut${this.props.index}" />`,
'<i class="dropdown icon"></i>',
'<div class="default text">Sociedad</div>',
'<div class="menu">',
...sociedades.map(sociedad => {
return [
`<div class="item" data-value="${sociedad.rut}">${sociedad.razon}</div>`
].join("\n")
}),
'</div>',
'</div>',
'</div>',
'<div class="four wide field">',
'<label>Banco</label>',
`<div class="ui search selection dropdown" id="${this.ids.banco}${this.props.index}">`,
`<input type="hidden" name="banco_id${this.props.index}" />`,
'<i class="dropdown icon"></i>',
'<div class="default text">Banco</div>',
'<div class="menu"></div>',
'</div>',
'</div>',
'<div class="field">',
'<label>Mes</label>',
`<div class="ui calendar" id="${this.ids.mes}${this.props.index}">`,
'<div class="ui input left icon">',
'<i class="calendar icon"></i>',
`<input type="text" name="mes${this.props.index}" placeholder="Mes" />`,
'</div>',
'</div>',
'</div>',
'<div class="field">',
'<label>Archivo</label>',
`<input type="file" class="ui invisible file input" name="archivo${this.props.index}" id="archivo${this.props.index}" />`,
`<label for="archivo${this.props.index}" class="ui icon button" id="archivo_button">`,
'<i class="file icon"></i>',
'</label>',
'</div>',
]
if (this.props.index > 1) {
output.push(...[
'<div class="field">',
'<label>&nbsp;</label>',
`<button class="ui red icon button remove" data-idx="${this.props.index}">`,
'<i class="trash icon"></i>',
'</button>',
'</div>',
])
}
output.push('</div>')
return output.join("\n")
}
activate() {
$(`#${this.ids.sociedad}${this.props.index}`).dropdown({
onChange: (value, text, $choice) => {
cartolas.fetch().bancos(value).then(response => {
if (!response) {
return
}
return response.json().then(data => {
const dropdown = $(`#${this.ids.banco}${this.props.index}`)
dropdown.dropdown('clear')
dropdown.dropdown('change values', data.cuentas.map(cuenta => {
const desc = [cuenta.banco.nombre, cuenta.cuenta].join(' - ')
return {
name: desc,
value: cuenta.id,
text: desc
}
}))
})
})
}
})
$(`#${this.ids.banco}${this.props.index}`).dropdown()
const cdo = structuredClone(calendar_date_options)
cdo.type = 'month'
$(`#${this.ids.mes}${this.props.index}`).calendar(cdo)
if (this.props.index > 1) {
$(`.${this.ids.buttons.remove}[data-idx="${this.props.index}"]`).click(clickEvent => {
const index = $(clickEvent.currentTarget).data('idx')
this.remove(index)
})
}
}
remove(idx) {
$(`.fields[data-idx=${idx}]`).remove()
cartolas.data.cartolas = cartolas.data.cartolas.filter(cartola => cartola.props.index !== idx)
cartolas.draw().form()
}
}
class Movimiento {
props
constructor({id, sociedad, fecha, glosa, cargo, abono, saldo, categoria, detalle, centro_costo, rut, nombres,
identificador, relacionado, relacionadoType, nuevo = false, obsoleto = false}) {
this.props = {
id,
sociedad,
fecha,
glosa,
cargo,
abono,
saldo,
categoria,
detalle,
centro_costo,
rut,
nombres,
identificador,
relacionado,
relacionadoType,
nuevo,
obsoleto
}
}
draw({formatters}) {
const fecha = new Date(this.props.fecha)
let nombre = ''
if (this.props.nombres) {
if (this.props.relacionado) {
let type = this.props.relacionadoType
type = type.charAt(0).toUpperCase() + type.slice(1)
nombre = `<span data-tooltip="${type}">${this.props.nombres}</span>`
} else {
nombre = this.props.nombres
}
}
let edit = ''
let color = ''
if (this.props.nuevo) {
color = ' class="green"'
}
if (this.props.obsoleto) {
color = ' class="red"'
edit = `<button class="ui tertiary red icon button remove_movimiento" data-id="${this.props.id}"><i class="remove icon"></i></button>`
}
return [
`<tr${color}>`,
`<td>${this.props.sociedad.sigla}</td>`,
`<td>${formatters.date.format(fecha)}</td>`,
`<td>${this.props.glosa}</td>`,
`<td class="right aligned">${formatters.number.format(this.props.cargo ?? 0)}</td>`,
`<td class="right aligned">${formatters.number.format(this.props.abono ?? 0)}</td>`,
`<td class="right aligned">${formatters.number.format(this.props.saldo)}</td>`,
`<td class="center aligned">${this.props.centro_costo ?? ''}</td>`,
`<td>${this.props.categoria ?? ''}</td>`,
`<td>${this.props.detalle ?? ''}</td>`,
`<td>${this.props.rut ?? ''}</td>`,
`<td>${nombre}</td>`,
`<td class="center aligned">${this.props.identificador ?? ''}</td>`,
`<td class="right aligned">${edit}</td>`,
'</tr>'
].join("\n")
}
}
const cartolas = {
ids: {},
data: {
sociedades: {!! json_encode($inmobiliarias) !!},
bancos: {!! json_encode($bancos) !!},
cartolas: [],
movimientos: [],
},
formatters: {
number: new Intl.NumberFormat('es-CL', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
date: new Intl.DateTimeFormat('es-CL', {dateStyle: 'short', timeStyle: 'short'})
},
add() {
return {
cartola: () => {
const idx = cartolas.data.cartolas.length + 1
const cartola = new Cartola({index: idx, ids: cartolas.ids.cartolas})
cartolas.data.cartolas.push(cartola)
cartolas.draw().form()
}
}
},
remove() {
return {
movimiento: id => {
const url = `{{$urls->api}}/contabilidad/movimiento/${id}`
const method = 'delete'
APIClient.fetch(url, {method}).then(response => {
if (!response.status) {
return
}
const index = this.data.movimientos.findIndex(movimiento => movimiento.id === response.movimiento.id)
this.data.movimientos.splice(index, 1)
this.draw().movimientos()
})
}
}
},
draw() {
return {
form: () => {
const form = $(this.ids.form)
form.empty()
form.append(`<input type="hidden" name="cartolas" value="${this.data.cartolas.length}" />`)
this.data.cartolas.forEach(cartola => {
form.append(cartola.draw({sociedades: this.data.sociedades, bancos: this.data.bancos}))
cartola.activate()
})
},
movimientos: () => {
const table = $(this.ids.movimientos)
const tbody = table.find('tbody')
tbody.empty()
this.data.movimientos.forEach(movimiento => {
tbody.append(movimiento.draw({formatters: this.formatters}))
})
table.show()
Object.values(document.getElementsByClassName('remove_movimiento')).forEach(element => {
element.addEventListener('click', clickEvent => {
const id = clickEvent.currentTarget.dataset['id']
this.remove().movimiento(id)
})
})
}
}
},
fetch() {
return {
bancos: sociedad_rut => {
const url = `{{$urls->api}}/inmobiliaria/${sociedad_rut}/cuentas`
return fetchAPI(url)
}
}
},
import() {
return {
cartolas: () => {
const url = '{{$urls->api}}/contabilidad/cartolas/importar'
const method = 'post'
const body = new FormData()
this.data.cartolas.forEach(cartola => {
Object.entries(cartola.data).forEach(([key, value]) => {
const name = `${key}[${cartola.props.index - 1}]`
body.append(name, value)
})
})
APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(data => {
if (data.errors.length > 0) {
data.errors.forEach(errorData => {
console.error(errorData)
})
}
this.data.movimientos = data.movimientos.map(movimiento => new Movimiento(movimiento))
this.draw().movimientos()
})
}).catch(error => {
const table = $(this.ids.movimientos)
const tbody = table.find('tbody')
tbody.empty()
table.hide()
}).finally(() => {
$(this.ids.loader).hide()
})
}
}
},
setup(ids) {
this.ids = ids
this.add().cartola()
$(this.ids.buttons.add).click(() => {
this.add().cartola()
})
$(this.ids.loader).hide()
$(this.ids.form).submit(submitEvent => {
submitEvent.preventDefault()
$(this.ids.loader).show()
this.import().cartolas()
return false
})
$(this.ids.buttons.process).click(() => {
$(this.ids.form).submit()
})
}
}
$(document).ready(() => {
cartolas.setup({
form: '#cartola_form',
buttons: {
add: '#add_button',
process: '#process_button'
},
movimientos: '#movimientos',
loader: '#loader',
cartolas: {
sociedad: 'sociedad',
banco: 'banco',
mes: 'mes',
buttons: {
remove: 'remove'
}
}
})
})
</script>
@endpush

View File

@ -14,9 +14,10 @@
<i class="dropdown icon"></i>
<div class="default text">Inmobiliaria</div>
<div class="menu">
@foreach ($inmobiliarias as $inmobiliaria)
<div class="item" data-value="{{$inmobiliaria->rut}}">{{$inmobiliaria->razon}}</div>
@endforeach
@foreach ($inmobiliarias as $inmobiliaria)
<div class="item"
data-value="{{$inmobiliaria->rut}}">{{$inmobiliaria->razon}}</div>
@endforeach
</div>
</div>
</div>
@ -40,7 +41,7 @@
</div>
<div class="field">
<label for="file">Cartola</label>
<input type="file" name="file" id="file" class="ui invisible file input" />
<input type="file" name="file" id="file" class="ui invisible file input"/>
<label for="file" class="ui icon button">
<i class="file icon"></i>
Cargar
@ -119,20 +120,20 @@
mes: '',
movimientos: [],
centrosCostos: {
ingresos: JSON.parse('{!! json_encode(array_values(array_map(function(\Incoviba\Model\CentroCosto $centroCosto) {
ingresos: JSON.parse('{!! json_encode(array_values(array_map(function(\Incoviba\Model\Contabilidad\CentroCosto $centroCosto) {
return [
'id' => $centroCosto->id,
'descripcion' => $centroCosto->descripcion
];
}, array_filter($centrosCostos, function(\Incoviba\Model\CentroCosto $centroCosto) {
}, array_filter($centrosCostos, function(\Incoviba\Model\Contabilidad\CentroCosto $centroCosto) {
return $centroCosto->tipoCentro->descripcion === 'Ingreso';
})))) !!}'),
egresos: JSON.parse('{!! json_encode(array_values(array_map(function(\Incoviba\Model\CentroCosto $centroCosto) {
egresos: JSON.parse('{!! json_encode(array_values(array_map(function(\Incoviba\Model\Contabilidad\CentroCosto $centroCosto) {
return [
'id' => $centroCosto->id,
'descripcion' => $centroCosto->descripcion
];
}, array_filter($centrosCostos, function(\Incoviba\Model\CentroCosto $centroCosto) {
}, array_filter($centrosCostos, function(\Incoviba\Model\Contabilidad\CentroCosto $centroCosto) {
return $centroCosto->tipoCentro->descripcion === 'Egreso';
})))) !!}'),
}
@ -170,7 +171,11 @@
return
}
$(this.ids.form.banco).dropdown('change values', json.cuentas.map(cuenta => {
return {value: cuenta.banco.id, text: cuenta.banco.nombre, name: cuenta.banco.nombre}
return {
value: cuenta.banco.id,
text: cuenta.banco.nombre,
name: cuenta.banco.nombre
}
})).dropdown('refresh')
})
})
@ -247,12 +252,12 @@
const movimientos = this.data.movimientos.map((movimiento, idx) => {
const temp = structuredClone(movimiento)
temp.fecha = movimiento.fecha.toISOString()
let centro = $(".centro[data-index='" + (idx+1) + "']").dropdown('get value')
let centro = $(".centro[data-index='" + (idx + 1) + "']").dropdown('get value')
if (centro.length === 0) {
centro = ''
}
temp.centro_costo = centro
let detalle = $("[name='detalle" + (idx+1) + "']").val()
let detalle = $("[name='detalle" + (idx + 1) + "']").val()
if (typeof detalle === 'undefined') {
detalle = ''
}
@ -294,7 +299,10 @@
month: 'numeric',
day: 'numeric'
})
const numberFormatter = new Intl.NumberFormat('es-CL', {minimumFractionDigits: 0, maximumFractionDigits: 0})
const numberFormatter = new Intl.NumberFormat('es-CL', {
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
this.data.movimientos.forEach((row, idx) => {
tbody.append(
$('<tr></tr>').append(
@ -322,7 +330,7 @@
})
table.DataTable(this.dataTableConfig)
},
centrosDropdown: (idx, ingreso=true) => {
centrosDropdown: (idx, ingreso = true) => {
const menu = $('<div></div>').addClass('menu')
let centros = this.data.centrosCostos.ingresos
if (!ingreso) {

View File

@ -0,0 +1,225 @@
@extends('layout.base')
@section('page_content')
<div class="ui container">
<h1 class="ui header">
Cuadratura de Cartola Mensual
</h1>
<form class="ui form" id="cuadratura_form">
<div class="ui grid">
<div class="fourteen wide column">
<div class="fields">
<div class="five wide field">
<label>Inmobiliaria</label>
<div class="ui selection search dropdown" id="inmobiliaria">
<input type="hidden" name="inmobiliaria"/>
<i class="dropdown icon"></i>
<div class="default text">Inmobiliaria</div>
<div class="menu">
@foreach ($inmobiliarias as $inmobiliaria)
<div class="item" data-value="{{$inmobiliaria->rut}}">{{$inmobiliaria->razon}}</div>
@endforeach
</div>
</div>
</div>
<div class="two wide field">
<label>Banco</label>
<div class="ui selection search dropdown" id="banco">
<input type="hidden" name="banco"/>
<i class="dropdown icon"></i>
<div class="default text">Banco</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>Mes</label>
<div class="ui calendar" id="mes">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="mes"/>
</div>
</div>
</div>
<div class="field">
<label for="file">Cartola</label>
<input type="file" name="file" id="file" class="ui invisible file input" />
<label for="file" class="ui icon button">
<i class="file icon"></i>
Cargar
</label>
</div>
</div>
</div>
<div class="two wide middle aligned column">
<button class="ui icon button">
Procesar
<i class="sync icon"></i>
</button>
</div>
</div>
</form>
<div class="ui two columns grid">
<div class="column"></div>
<div class="right aligned column">
<div class="ui inline active loader" id="loader"></div>
</div>
</div>
</div>
<div class="ui fluid container" id="movimientos"></div>
@endsection
@include('layout.head.styles.datatables')
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script>
class LibroMayor {
props
constructor(props) {
this.props = props
}
}
class Cartola {
}
const movimientos = {
ids: {},
data: {
inmobiliaria: {
rut: '',
razon: ''
},
banco: {
id: '',
nombre: ''
}
},
get() {
return {
bancos: inmobiliaria_rut => {
const url = '{{$urls->api}}/inmobiliaria/' + inmobiliaria_rut + '/cuentas'
$(this.ids.loader).show()
return fetchAPI(url).then(response => {
$(this.ids.loader).hide()
if (!response) {
return
}
return response.json().then(json => {
if (json.cuentas.length === 0) {
return
}
$(this.ids.inputs.banco).dropdown('change values', json.cuentas.map(cuenta => {
return {value: cuenta.banco.id, text: cuenta.banco.nombre, name: cuenta.banco.nombre}
})).dropdown('refresh')
})
})
},
firstDate: inmobiliaria_rut => {
const url = '{{$urls->api}}/inmobiliaria/' + inmobiliaria_rut + '/proyectos'
$(this.ids.loader).show()
return fetchAPI(url).then(response => {
$(this.ids.loader).hide()
if (!response) {
return
}
return response.json().then(json => {
if (json.proyectos.length === 0) {
return
}
const min = json.proyectos.reduce((min, proyecto) => {
const date = new Date(proyecto.current_estado.fecha.date)
if (min > date.getTime()) {
return date.getTime()
}
return min
}, (new Date()).getTime())
$(this.ids.inputs.mes).calendar('set minDate', new Date(min))
})
})
}
}
},
parse() {
return {
cartola: submitEvent => {
submitEvent.preventDefault()
const body = new FormData(document.getElementById('asignar_form'))
body.set('mes', $(this.ids.inputs.mes).calendar('get date').toISOString())
const url = '{{$urls->api}}/contabilidad/cartolas/procesar'
$(this.ids.loader).show()
fetchAPI(url, {method: 'post', body}).then(response => {
$(this.ids.loader).hide()
if (!response) {
return
}
return response.json().then(json => {
if (json.movimientos.length === 0) {
return
}
this.data.movimientos = []
json.movimientos.forEach((row, idx) => {
const fecha = new Date(row.fecha)
fecha.setDate(fecha.getDate() + 1)
this.data.movimientos[idx] = {
fecha: fecha,
glosa: row.glosa,
documento: row.documento,
cargo: row.cargo,
abono: row.abono,
}
})
this.draw().cartola()
})
})
return false
}
}
},
setup(ids) {
this.ids = ids
$(this.ids.inputs.inmobiliaria).dropdown({
fireOnInit: true,
onChange: (value, text, $choice) => {
this.data.inmobiliaria.rut = value
this.data.inmobiliaria.razon = text
this.get().bancos(value)
this.get().firstDate(value)
},
})
$(this.ids.inputs.banco).dropdown({
fireOnInit: true,
onChange: (value, text, $choice) => {
this.data.banco.id = value
this.data.banco.nombre = text
}
})
$(this.ids.loader).hide()
calendar_date_options['type'] = 'month'
const lastMonth = new Date()
lastMonth.setDate(0)
calendar_date_options['maxDate'] = lastMonth
calendar_date_options['onChange'] = (date, text, mode) => {
this.data.mes = text
}
$(this.ids.inputs.mes).calendar(calendar_date_options)
$(this.ids.form).submit(this.parse().cartola)
//$(this.ids.button).click(this.export().cartola)
}
}
$(document).ready(() => {
movimientos.setup({
movimientos: '#movimientos',
form: '#cuadratura_form',
loader: '#loader',
inputs: {
inmobiliaria: '#inmobiliaria',
banco: '#banco',
mes: '#mes',
cartola: '#file'
}
})
})
</script>
@endpush

View File

@ -1,14 +1,23 @@
@extends('layout.base')
@push('page_styles')
<style>
tr.bold > th {
font-weight: bold !important;
}
</style>
@endpush
@section('page_content')
<div class="ui container">
<h1 class="ui centered header">Informe de Tesorería</h1>
<h4 class="ui centered sub header">{{$fecha->format('d M Y')}}</h4>
<div class="ui grid">
<div class="three wide column">
<a href="/contabilidad/informes/xlsx/tesoreria/{{$fecha->format('Y-m-d')}}" target="_blank" style="color: inherit;">
<a href="/contabilidad/informes/xlsx/tesoreria/{{$fecha->format('Y-m-d')}}" target="_blank"
style="color: inherit;">
<div class="ui inverted green center aligned segment">
Descargar en Excel <i class="file excel icon"></i>
Descargar en Excel <i class="file excel icon"></i>
</div>
</a>
</div>
@ -19,7 +28,11 @@
<table class="ui collapsing simple table">
<tr>
<td>Informe anterior</td>
<td>{{$anterior->format('d/m/Y')}}</td>
<td>
<a href="{{$urls->base}}/contabilidad/informes/tesoreria/{{$anterior->format('Y-m-d')}}">
{{$anterior->format('d/m/Y')}}
</a>
</td>
</tr>
<tr>
<td>Informe actual</td>
@ -40,6 +53,17 @@
</tr>
</table>
</div>
@if ($siguiente !== null)
<div class="five wide column"></div>
<div class="right aligned two wide column">
<div class="ui segment">
Siguiente
<a href="{{$urls->base}}/contabilidad/informes/tesoreria/{{$siguiente->format('Y-m-d')}}">
{{$siguiente->format('d/m/Y')}} >>
</a>
</div>
</div>
@endif
</div>
<table class="ui striped table">
<thead>
@ -60,27 +84,31 @@
</tr>
</thead>
<tbody>
@foreach ($informes['inmobiliarias'] as $inmobiliaria_rut => $informe)
@foreach ($informes['sociedades'] as $sociedad_rut => $informe)
@foreach ($informe->cuentas as $i => $cuenta)
<tr>
@if ($i === 0)
<td rowspan="{{count($informe->cuentas)}}">{{$informe->inmobiliaria->razon}}</td>
@endif
<td>{{$cuenta->banco}}</td>
<td>{{$cuenta->numero}}</td>
<td class="right aligned">{{$format->pesos($cuenta->anterior)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->actual)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->diferencia())}}</td>
<td class="right aligned">{{$format->pesos($cuenta->ffmm)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->deposito)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->saldo())}}</td>
@if ($i === 0)
<td class="right aligned" rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->total())}}</td>
<td class="right aligned" rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->ffmm())}}</td>
<td class="right aligned" rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->deposito())}}</td>
<td class="right aligned" rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->caja())}}</td>
@endif
</tr>
<tr>
@if ($i === 0)
<td rowspan="{{count($informe->cuentas)}}">{{$informe->sociedad->razon}}</td>
@endif
<td>{{$cuenta->banco}}</td>
<td>{{$cuenta->numero}}</td>
<td class="right aligned">{{$format->pesos($cuenta->anterior)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->actual)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->diferencia())}}</td>
<td class="right aligned">{{$format->pesos($cuenta->ffmm)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->deposito)}}</td>
<td class="right aligned">{{$format->pesos($cuenta->saldo())}}</td>
@if ($i === 0)
<td class="right aligned"
rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->total())}}</td>
<td class="right aligned"
rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->ffmm())}}</td>
<td class="right aligned"
rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->deposito())}}</td>
<td class="right aligned"
rowspan="{{count($informe->cuentas)}}">{{$format->pesos($informe->caja())}}</td>
@endif
</tr>
@endforeach
@endforeach
</tbody>
@ -123,7 +151,7 @@
@foreach ($movimientos as $ms)
@foreach ($ms as $movimiento)
<tr>
<td >{{$movimiento->cuenta->inmobiliaria->razon}}</td>
<td>{{$movimiento->cuenta->inmobiliaria->razon}}</td>
<td class="right aligned">{{$format->pesos($movimiento->abono)}}</td>
<td class="right aligned">{{$format->pesos($movimiento->cargo)}}</td>
<td>{{$movimiento->fecha->format('d/m/Y')}}</td>
@ -142,7 +170,7 @@
</tr>
@foreach ($movimientos as $movimiento)
<tr>
<td >{{$movimiento->cuenta->inmobiliaria->razon}}</td>
<td>{{$movimiento->cuenta->inmobiliaria->razon}}</td>
<td class="right aligned">{{$format->pesos($movimiento->abono)}}</td>
<td class="red right aligned">{{$format->pesos($movimiento->cargo)}}</td>
<td>{{$movimiento->fecha->format('d/m/Y')}}</td>

View File

@ -0,0 +1,140 @@
@extends('layout.base')
@include('layout.head.styles.datatables')
@include('layout.head.styles.datatables.searchbuilder')
@include('layout.head.styles.datatables.buttons')
@push('page_styles')
<style>
#table_container {
margin-left: 1rem;
margin-right: 1rem;
}
</style>
@endpush
@section('page_content')
<div class="ui container">
<h1 class="ui header">Movimientos Contables</h1>
<form class="ui form" id="movimientos_form">
<div class="fields">
<div class="six wide field">
<label for="sociedad">Sociedad</label>
<div class="ui selection search multiple dropdown" id="sociedades">
<input type="hidden" name="sociedad[]"/>
<i class="dropdown icon"></i>
<div class="default text">Sociedad</div>
<div class="menu">
@foreach($sociedades as $sociedad)
<div class="item" data-value="{{$sociedad->rut}}">{{$sociedad->razon}}</div>
@endforeach
</div>
</div>
</div>
<div class="three wide field">
<label for="mes">Mes</label>
<div class="ui calendar" id="mes">
<div class="ui right icon input">
<input type="text" name="mes"/>
<i class="calendar icon"></i>
</div>
</div>
</div>
<div class="field">
<label></label>
<button class="ui circular icon button">
<i class="sync alternate icon"></i>
</button>
</div>
</div>
</form>
</div>
<div id="table_container">
<table id="tabla_movimientos" class="ui compact table">
<thead>
<tr>
<th>Sigla</th>
<th>Banco</th>
<th>Cuenta</th>
<th>Fecha</th>
<th>ISO Fecha</th>
<th>Mes</th>
<th>Año</th>
<th class="right aligned">Valor</th>
<th>Valor</th>
<th>Glosa</th>
<th>Centro de Costo</th>
<th>Categoría</th>
<th>Detalle</th>
<th>RUT</th>
<th>Nombre</th>
<th>Identificador</th>
<th>Editar</th>
</tr>
</thead>
<tbody id="movimientos"></tbody>
</table>
</div>
@include('contabilidad.movimientos.edit_modal')
@endsection
@include('layout.body.scripts.datatables')
@include('layout.body.scripts.datatables.searchbuilder')
@include('layout.body.scripts.datatables.buttons')
@include('layout.body.scripts.rut')
@push('page_scripts')
<script>
class CentrosCostos {
static data = []
static get() {
if (this.data.length === 0) {
this.data = JSON.parse('{!! json_encode($centros) !!}')
}
return this.data
}
}
</script>
@endpush
@push('page_scripts')
@include('contabilidad.movimientos.scripts.event_handler')
@include('contabilidad.movimientos.scripts.loading_handler')
@include('contabilidad.movimientos.scripts.control_form')
@include('contabilidad.movimientos.scripts.movimientos_table')
@include('contabilidad.movimientos.scripts.edit_modal')
@include('contabilidad.movimientos.scripts.edit_form')
@include('contabilidad.movimientos.scripts.movimientos_handler')
@include('contabilidad.movimientos.scripts.results_handler')
<script>
const app = {
handlers: {},
setup() {
this.handlers.results = new ResultsHandler()
this.handlers.loading = new LoadingHandler({id: '#movimientos_form'})
this.handlers.movimientos = new MovimientosHandler({
urls: {
get: '{{$urls->api}}/contabilidad/movimientos/sociedad/mes',
edit: '{{$urls->api}}/contabilidad/movimientos/edit',
remove: '{{$urls->api}}/contabilidad/movimiento',
},
loadingHandler: this.handlers.loading,
resultsHandler: this.handlers.results
})
this.handlers.forms = {
edit: new EditForm({ids: {modal: '#movimientos_modal', data: '#movimientos_data', form: '#movimientos_edit',}})
}
this.handlers.modals = {
edit: new EditModal({ids: {modal: '#movimientos_modal'}, editForm: this.handlers.forms.edit})
}
this.handlers.events = new EventHandler({movimientosHandler: this.handlers.movimientos, modalHandler: this.handlers.modals.edit})
this.handlers.table = new MovimientosTable({ids: {table: '#tabla_movimientos', buttons: {edit: '.edit_button', remove: '.remove_button'}}, eventHandler: this.handlers.events})
this.handlers.forms.control = new ControlForm({ids: {form: '#movimientos_form', sociedades: '#sociedades', mes: '#mes',},})
this.handlers.forms.edit.setup()
}
}
$(document).ready(() => {
app.setup()
})
</script>
@endpush

View File

@ -0,0 +1,67 @@
<div class="ui fluid modal" id="movimientos_modal">
<div class="content">
<h3 class="header">
Editar Movimiento
</h3>
<div class="ui divider"></div>
<div class="ui grid container" id="modal_info">
<div class="column sociedad"></div>
<div class="column banco"></div>
<div class="column cuenta"></div>
<div class="column fecha"></div>
<div class="column valor"></div>
<div class="column glosa"></div>
</div>
<div class="ui divider"></div>
<form class="ui form" id="movimientos_edit">
<input type="hidden" name="index"/>
<input type="hidden" name="id"/>
<div class="fields">
<div class="field">
<label for="centro_costo">Centro de Costo</label>
<div class="ui selection search dropdown" id="centro_costo">
<input type="hidden" name="centro_id"/>
<i class="dropdown icon"></i>
<div class="default text">Centro de Costo</div>
<div class="menu">
@foreach($centros as $centroCosto)
<div class="item" data-value="{{$centroCosto->id}}">{{$centroCosto->id}} - {{$centroCosto->descripcion}}</div>
@endforeach
</div>
</div>
</div>
<div class="field">
<label>Categoría</label>
<input type="text" name="categoria" placeholder="Categoría" />
</div>
<div class="field">
<label>Identificador</label>
<input type="text" name="identificador" placeholder="Identificador" />
</div>
</div>
<div class="fields">
<div class="field">
<label>RUT</label>
<div class="ui right labeled input">
<input type="text" name="rut" placeholder="RUT" />
<div class="ui basic label">-<span id="digito"></span></div>
</div>
</div>
<div class="field">
<label>Nombre</label>
<input type="text" name="nombre" placeholder="Nombre" />
</div>
</div>
<div class="field">
<label for="detalle">Detalle</label>
<div class="ui input">
<textarea id="detalle" name="detalle"></textarea>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui red cancel button">Cancelar</div>
<div class="ui green approve success button">Guardar</div>
</div>
</div>

View File

@ -0,0 +1,22 @@
<script>
class ControlForm {
props
constructor({ids,}) {
this.props = {
ids,
}
$(this.props.ids.sociedades).dropdown()
const cdo = structuredClone(calendar_date_options)
cdo['type'] = 'month'
cdo['maxDate'] = new Date()
$(this.props.ids.mes).calendar(cdo)
$(this.props.ids.form).on('submit', {
sociedades_id: this.props.ids.sociedades,
mes_id: this.props.ids.mes,
handler: app.handlers.events
}, app.handlers.events.get().movimientos)
}
}
</script>

View File

@ -0,0 +1,98 @@
<script>
class EditForm {
props
fields
constructor({ids}) {
this.props = {
ids,
movimientos: []
}
this.fields = [
{
name: 'index',
value: index => index
},
{
name: 'id',
value: movimiento => movimiento.id
},
{
id: 'centro_costo',
name: 'centro_id',
type: 'dropdown',
value: movimiento => movimiento.detalles.centro_costo.id ?? '',
values: CentrosCostos.get().map(centroCosto => {
return {
value: centroCosto.id,
name: `${centroCosto.id} - ${centroCosto.descripcion}`,
text: `${centroCosto.id} - ${centroCosto.descripcion}`,
}
})
},
{
name: 'rut',
type: 'text',
value: movimiento => movimiento.detalles.rut ?? ''
},
{
id: 'digito',
value: movimiento => movimiento.detalles.digito ?? ''
},
{
name: 'nombre',
type: 'text',
value: movimiento => movimiento.detalles.nombres ?? ''
},
{
name: 'categoria',
type: 'text',
value: movimiento => movimiento.detalles.categoria ?? ''
},
{
name: 'detalle',
type: 'text',
value: movimiento => movimiento.detalles.detalle ?? ''
},
{
name: 'identificador',
type: 'text',
value: movimiento => movimiento.detalles.identificador ?? ''
}
]
this.fields.filter(field => typeof field.values !== 'undefined').forEach(field => {
$(this.props.ids.form).find(`#${field.id}`).dropdown('change values', field.values)
})
}
reset() {
$(this.props.ids.form).form('reset')
}
show({modal, movimiento, index}) {
modal.find(this.props.ids.data).show()
this.fields.forEach(field => {
if (typeof field.name !== 'undefined') {
switch (field.type) {
case 'dropdown':
modal.find(`#${field.id}`).dropdown('set selected', field.value(movimiento))
break
default:
if (field.name === 'index') {
modal.find(`[name='${field.name}']`).val(index)
return
}
modal.find(`[name='${field.name}']`).val(field.value(movimiento))
break
}
return
}
modal.find(`#${field.id}`).text(field.value(movimiento))
})
}
setup() {
$(this.props.ids.form).on('submit', {
form_id: this.props.ids.form,
}, app.handlers.events.edit().modal)
}
}
</script>

View File

@ -0,0 +1,74 @@
<script>
class EditModal {
props
info
constructor({ids, editForm}) {
this.props = {
ids,
editForm,
movimientos: []
}
this.info = [
{
className: 'sociedad',
width: 'three wide',
value: movimiento => `<b>Sociedad</b>: ${movimiento.cuenta.inmobiliaria.sigla}`
},
{
className: 'banco',
width: 'three wide',
value: movimiento => `<b>Banco</b>: ${movimiento.cuenta.banco.nombre}`
},
{
className: 'cuenta',
width: 'three wide',
value: movimiento => `<b>Cuenta</b>: ${movimiento.cuenta.cuenta}`
},
{
className: 'fecha',
width: 'three wide',
value: movimiento => `<b>Fecha</b>: ${movimiento.fecha}`
},
{
className: 'valor',
width: 'three wide',
value: movimiento => `<b>Valor</b>: ${movimiento.abono - movimiento.cargo}`
},
{
className: 'glosa',
width: 'ten wide',
value: movimiento => `<b>Glosa</b>: ${movimiento.glosa}`
},
]
const $info = $(this.props.ids.modal).find('#modal_info')
$info.empty()
this.info.forEach(field => {
$info.append(`<div class="${field.width} column ${field.className}"></div>`)
})
$(this.props.ids.modal).modal({
onApprove: $element => {
$(this.props.editForm.props.ids.form).submit()
}
})
}
reset() {
this.info.forEach(info => {
$(this.props.ids.modal).find(`.${info.className}`).text('')
})
this.props.editForm.reset()
}
show({movimiento, index}) {
$(this.props.ids.modal).modal('show')
this.info.forEach(info => {
$(this.props.ids.modal).find(`.${info.className}`).html(info.value(movimiento))
})
this.props.editForm.show({modal: $(this.props.ids.modal), movimiento, index})
}
hide() {
$(this.props.ids.modal).modal('hide')
this.reset()
}
}
</script>

View File

@ -0,0 +1,54 @@
<script>
class EventHandler {
props
constructor({movimientosHandler, modalHandler}) {
this.props = {
movimientosHandler,
modalHandler
}
}
get() {
return {
movimientos: submitEvent => {
submitEvent.preventDefault()
submitEvent.data.handler.props.movimientosHandler.get().movimientos({
sociedades_ruts: $(submitEvent.data.sociedades_id).dropdown('get values'),
mes: $(submitEvent.data.mes_id).calendar('get date')
}).then(() => {
app.handlers.table.draw().table(app.handlers.results.props.parsed)
})
return false
}
}
}
edit() {
return {
movimiento: clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
const index = $(clickEvent.currentTarget).data('index')
const movimiento = this.props.movimientosHandler.find().id(id)
clickEvent.data.handler.props.modalHandler.show({movimiento, index})
},
modal: submitEvent => {
submitEvent.preventDefault()
const form = submitEvent.currentTarget
const data = new FormData(form)
app.handlers.movimientos.edit().movimiento(data).then(() => {
app.handlers.table.draw().table(app.handlers.results.props.parsed)
})
return false
}
}
}
remove() {
return {
movimiento: clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
app.handlers.movimientos.remove().movimiento(id).then(() => {
app.handlers.table.draw().table(app.handlers.results.props.parsed)
})
}
}
}
}
</script>

View File

@ -0,0 +1,16 @@
<script>
class LoadingHandler {
props
constructor(ids) {
this.props = {
...ids
}
}
show() {
$(this.props.id).addClass('loading')
}
hide() {
$(this.props.id).removeClass('loading')
}
}
</script>

View File

@ -0,0 +1,86 @@
<script>
class MovimientosHandler {
props
constructor({urls, loadingHandler, resultsHandler}) {
this.props = {
urls,
loadingHandler,
resultsHandler
}
}
get() {
return {
movimientos: ({sociedades_ruts, mes}) => {
this.props.loadingHandler.show()
const method = 'post'
const body = new FormData()
sociedades_ruts.forEach(sociedad_rut => {
body.append('sociedades_ruts[]', sociedad_rut)
})
body.set('mes', [mes.getFullYear(), mes.getMonth() + 1, mes.getDate()].join('-'))
return APIClient.fetch(this.props.urls.get, {method, body}).then(response => {
if (!response) {
throw new Error('No se pudo obtener los movimientos')
}
return response.json().then(json => {
this.movimientos = json.movimientos
this.props.resultsHandler.props.movimientos = json.movimientos
this.props.resultsHandler.parse().movimientos()
return json.movimientos
})
}).catch(error => {
console.error(error)
}).finally(() => {
this.props.loadingHandler.hide()
})
}
}
}
edit() {
return {
movimiento: (data) => {
data.set('digito', Rut.digitoVerificador(data.get('rut')))
const method = 'post'
return APIClient.fetch(this.props.urls.edit, {method, body: data}).then(response => {
if (!response) {
throw new Error('No se pudo editar el movimiento')
}
return response.json().then(json => {
this.props.resultsHandler.props.movimientos = this.props.resultsHandler.props.movimientos.map(movimiento => {
if (movimiento.id === json.movimiento.id) {
movimiento.detalles = json.movimiento.detalles
}
return movimiento
})
this.props.resultsHandler.parse().movimientos()
})
})
}
}
}
remove() {
return {
movimiento: id => {
const method = 'delete'
const url = this.props.urls.remove.replace(/\/$/, '') + '/' + id
return APIClient.fetch(url, {method}).then(response => {
if (!response) {
throw new Error('No se pudo eliminar el movimiento')
}
return response.json().then(json => {
this.props.resultsHandler.props.movimientos = this.props.resultsHandler.props.movimientos.filter(movimiento => movimiento.id !== id)
this.props.resultsHandler.parse().movimientos()
})
})
}
}
}
find() {
return {
id: id => {
return this.props.resultsHandler.props.movimientos.find(movimiento => movimiento.id === id)
}
}
}
}
</script>

View File

@ -0,0 +1,231 @@
<script>
class MovimientosTable {
props
columns
constructor({ids, eventHandler}) {
this.props = {
ids,
eventHandler,
movimientos: [],
formatters: {
number: new Intl.NumberFormat('es-CL'),
date: new Intl.DateTimeFormat('es-CL', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
}),
},
}
this.columns = [
{title: 'Sigla', defs: {width: 'short', type: 'string'}, process: movimiento => movimiento.cuenta.inmobiliaria.sigla, args: ['movimiento'], searchable: true, export: true}, //0
{title: 'Banco', defs: {width: 'long', type: 'string'}, process: movimiento => movimiento.cuenta.banco.nombre, args: ['movimiento'], searchable: true, export: true},
{title: 'Cuenta', defs: {width: 'short', type: 'string'}, process: movimiento => movimiento.cuenta.cuenta, args: ['movimiento'], searchable: true, export: true},
{title: 'Fecha', defs: {width: 'long', type: 'string'}, process: fecha => this.props.formatters.date.format(fecha), args: ['fecha'], searchable: true, export: true, format: (value) => value.split('-').reverse().join('-')},
{title: 'ISO Fecha', defs: {visible: false}, process: fecha => [fecha.getFullYear(), fecha.getMonth() + 1, fecha.getDate()].join('-'), args: ['fecha']},
{title: 'Mes', defs: {width: 'short', type: 'string'}, process: fecha => fecha.getMonth() + 1, args: ['fecha'], searchable: true}, //5
{title: 'Año', defs: {width: 'long', type: 'string'}, process: fecha => fecha.getFullYear(), args: ['fecha'], searchable: true},
{title: 'Valor', defs: {className: 'dt-right', width: 'long', type: 'num'}, process: valor => this.props.formatters.number.format(valor), args: ['valor'], export: true, format: (value) => value.replace(/\./g, '')},
{title: 'ISO Valor', defs: {visible: false, type: 'num'}, args: ['valor']},
{title: 'Glosa', defs: {width: 'short', type: 'string'}, process: movimiento => movimiento.glosa, args: ['movimiento'], export: true},
{title: 'Centro de Costo', defs: {width: 'short', type: 'string'}, process: movimiento => (movimiento.detalles && movimiento.detalles.centro_costo) ? movimiento.detalles.centro_costo.id : '', args: ['movimiento'], searchable: true, export: true}, //10
{title: 'Categoría', defs: {width: 'short', type: 'string'}, process: movimiento => (movimiento.detalles) ? movimiento.detalles.categoria : '', args: ['movimiento'], searchable: true, export: true},
{title: 'Detalle', defs: {width: 'long', type: 'string'}, process: movimiento => (movimiento.detalles) ? movimiento.detalles.detalle : '', args: ['movimiento'], export: true},
{title: 'RUT', defs: {width: 'short', type: 'string'}, process: movimiento => (movimiento.detalles && movimiento.detalles.digito) ? `${this.props.formatters.number.format(movimiento.detalles.rut)}-${movimiento.detalles.digito}` : '', args: ['movimiento'], searchable: true, export: true},
{title: 'Nombre', defs: {width: 'short', type: 'string'}, args: ['nombre'], searchable: true, export: true},
{title: 'Identificador', defs: {width: 'short', type: 'string'}, process: movimiento => (movimiento.detalles) ? movimiento.detalles.identificador : '', args: ['movimiento'], export: true}, //15
{title: 'Editar', defs: {width: 'short', type: 'string'}, args: ['buttonsHTML']},
]
const N = this.columns.filter(column => typeof column.defs.visible === 'undefined' || column.defs.visible).length
const nShort = this.columns.filter(column => column.defs.width === 'short').length
const nLong = this.columns.filter(column => column.defs.width === 'long').length
const short = `${(nShort / N).toFixed(2)}%`
const long = `${(nLong / N).toFixed(2)}%`
this.columns = this.columns.map(column => {
if (typeof column.defs.width === 'undefined') {
return column
}
switch (column.defs.width) {
case 'short':
column.defs.width = short
break
case 'long':
column.defs.width = long
break
}
return column
})
const tr = $(this.props.ids.table).find('thead tr')
tr.empty()
this.columns.forEach(column => {
if (typeof column.defs.className === 'undefined') {
tr.append(`<th>${column.title}</th>`)
return
}
switch (column.defs.className) {
case 'dt-right':
tr.append(`<th class="right aligned">${column.title}</th>`)
break
case 'dt-center':
tr.append(`<th class="center aligned">${column.title}</th>`)
break
default:
tr.append(`<th>${column.title}</th>`)
break
}
})
let dtD = structuredClone(datatables_defaults)
const groupedColumns = Object.groupBy(this.columns, ({defs}) => Object.entries(defs).map((field, value) => `${field}: ${value}`).join(' - '))
const columnDefs = Object.values(groupedColumns).map(group => {
return {
targets: group.map(({title}) => title).map(title => this.columns.map(column => column.title).indexOf(title)),
...group[0].defs
}
})
const searchBuilderColumns = this.columns.filter(column => column.searchable)
.map(column => column.title).map(title => this.columns.map(column => column.title).indexOf(title))
const exportColumns = this.columns.filter(column => column.export)
.map(column => column.title).map(title => this.columns.map(column => column.title).indexOf(title))
const order = ['Sigla', 'Banco', 'ISO Fecha'].map(title => {
return [this.columns.map(column => column.title).indexOf(title), 'asc']
})
dtD = Object.assign(dtD, {
columnDefs,
order,
language: Object.assign(dtD.language, {
searchBuilder
}),
layout: {
top1: {
searchBuilder: {
columns: searchBuilderColumns,
}
},
top1End: {
buttons: [
{
extend: 'excelHtml5',
className: 'green',
text: 'Exportar a Excel <i class="file excel icon"></i>',
title: 'Movimientos Contables',
download: 'open',
exportOptions: {
columns: exportColumns,
format: {
body: (data, row, columnIdx, node) => {
const formats = this.columns.filter(columnDef => columnDef.format)
const match = formats.map(({title}) => title).indexOf(this.columns[columnIdx].title)
if (match > -1) {
return formats[match].format(data)
}
if (typeof data === 'string' && data.includes('<span data-tooltip')) {
return data.replace(/<span data-tooltip="(.*)">(.*)<\/span>/, '$2')
}
return data
}
}
},
customize: xlsx => {
const sheet = xlsx.xl.worksheets['sheet1.xml']
const columns = Object.values($('row[r="2"] t', sheet).map((idx, column) => column.textContent))
const columnStylesMap = {
Valor: '63',
Fecha: '15'
}
Object.entries(columnStylesMap).forEach((column, style) => {
const columnIndex = String.fromCharCode('A'.charCodeAt(0) + columns.indexOf(column))
$(`c[r^="${columnIndex}"]`, sheet).attr('s', style)
})
}
}
]
}
},
data: this.props.movimientos,
rowCallback: (row, data) => {
$(row).find(this.props.ids.buttons.edit).on('click', {handler: this.props.eventHandler}, this.props.eventHandler.edit().movimiento)
$(row).find(this.props.ids.buttons.remove).on('click', {handler: this.props.eventHandler}, this.props.eventHandler.remove().movimiento)
}
})
this.props.table = new DataTable(this.props.ids.table, dtD)
}
draw() {
return {
table: (movimientos) => {
const info = this.props.table.page.info()
this.props.table.clear().rows.add(this.draw().movimientos(movimientos)).draw()
this.props.table.page(info.page).draw(false)
$(this.props.ids.buttons.edit).on('click', {handler: this.props.eventHandler}, this.props.eventHandler.edit().movimiento)
$(this.props.ids.buttons.remove).on('click', {handler: this.props.eventHandler}, this.props.eventHandler.remove().movimiento)
},
movimientos: (movimientos) => {
const output = []
movimientos.forEach(movimiento => {
this.draw().movimiento({movimiento, output})
})
return output
},
movimiento: ({movimiento, output}) => {
const valor = movimiento.abono - movimiento.cargo
const fecha = movimiento.fecha
const buttons = {
edit: {
icon: 'edit'
},
remove: {
color: ' red',
icon: 'trash'
}
}
const buttonsHTML = Object.entries(buttons).map(([action, {color='', icon}]) => {
const className = this.props.ids.buttons[action].replace('.', '')
return `<button class="ui${color} icon button ${className}" data-id="${movimiento.id}" data-index="${movimiento.index}" data-type="movimiento"><i class="${icon} icon"></i></button>`
}).join('')
let nombre = ''
if (movimiento.detalles) {
if (movimiento.detalles.relacionado) {
let type = movimiento.detalles.relacionadoType
type = type.charAt(0).toUpperCase() + type.slice(1)
nombre = `<span data-tooltip="${type}">${movimiento.detalles.nombres}</span>`
} else {
nombre = movimiento.detalles.nombres
}
}
const params = {
movimiento,
fecha,
valor,
nombre,
buttonsHTML,
}
const data = {}
this.columns.forEach(column => {
if (column.process) {
const args = Object.entries(params).filter(([param, value]) => column.args.includes(param)).map(arr => arr[1])
data[column.title] = column.process(...args)
return
}
if (column.args) {
data[column.title] = Object.entries(params).filter(([param, value]) => column.args.includes(param)).map(arr => arr[1])[0]
return;
}
data[column.title] = params[column.title.toLowerCase()]
})
const values = []
this.columns.forEach(column => {
values.push(data[column.title])
})
output.push(values)
}
}
}
}
</script>

View File

@ -0,0 +1,37 @@
<script>
class ResultsHandler {
props
constructor() {
this.props = {
movimientos: [],
timezone: ((new Date()).getTimezoneOffset()) / -60
}
}
parse() {
return {
movimientos: () => {
const movimientos = this.props.movimientos
const data = []
movimientos.forEach((movimiento, index) => {
this.parse().movimiento({data, movimiento, index})
})
this.props.parsed = data
},
movimiento: ({data, movimiento, index}) => {
const fecha = new Date(movimiento.fecha + 'Z' + this.props.timezone)
data.push({
tipo: 'movimiento',
id: movimiento.id,
index,
cuenta: movimiento.cuenta,
fecha,
cargo: movimiento.cargo,
abono: movimiento.abono,
glosa: movimiento.glosa,
detalles: movimiento.detalles,
})
}
}
}
}
</script>

View File

@ -0,0 +1,53 @@
@extends('layout.base')
@section('page_content')
<div class="ui container">
<h1 class="ui header">Importar Informe de Tesorería</h1>
<form class="ui form" id="import_form" enctype="multipart/form-data">
<div class="ten wide field">
<label>Informe</label>
<div class="ui file action input">
<input type="file" name="file[]" id="file" accept=".xlsx" multiple placeholder="Informe" />
<label for="file" class="ui icon button">
<i class="file icon"></i>
</label>
</div>
</div>
<button class="ui icon button">
<i class="arrow up icon"></i>
Subir
</button>
</form>
</div>
@endsection
@push('page_scripts')
<script>
$(document).ready(() => {
$('#import_form').submit(submitEvent => {
submitEvent.preventDefault()
let formData = new FormData()
formData.append('file', $('#file')[0].files[0])
const url = '{{$urls->api}}/contabilidad/tesoreria/import'
const method = 'post'
const body = new FormData(submitEvent.currentTarget)
fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(data => {
console.log(data)
})
}).catch(error => {
console.error(error)
})
return false
})
})
</script>
@endpush

View File

@ -10,19 +10,17 @@
<div class="ui two column grid">
<div class="column">
@include('home.cuotas_por_vencer')
@include('home.cierres_vigentes')
</div>
<div class="column">
@include('home.alertas')
</div>
<div class="column">
@include('home.cierres_vigentes')
</div>
</div>
</div>
@endsection
@push('page_scripts')
<script type="text/javascript">
<script>
const cuotas = {
get: function() {
return {

View File

@ -2,7 +2,7 @@
<div class="ui divided list" id="alertas_escrituras"></div>
@push('page_scripts')
<script type="text/javascript">
<script>
const alertas_escrituras = {
id: '#alertas_escrituras',
data: {
@ -68,21 +68,6 @@
const index = this.data.proyectos.findIndex(proyecto => proyecto.id === data.proyecto_id)
this.data.proyectos[index].escrituras = data.escrituras
})
/*const index = this.data.proyectos.findIndex(proyecto => proyecto.id === proyecto_id)
if (proyecto_id === 3) {
this.data.proyectos[index].escrituras = {
firmar: 20,
pagar: 70,
abonar: 3
}
}
if (proyecto_id === 4) {
this.data.proyectos[index].escrituras = {
firmar: 0,
pagar: 0,
abonar: 2
}
}*/
}
}
},
@ -132,7 +117,7 @@
$('<div></div>').addClass('event').append(
$('<div></div>').addClass('content').html(tipo)
).append(
$('<div></div>').addClass('meta').html(total + '/' + full + ' ' + formatter.format(Math.round(total / full * 10000) / 100).padStart(2, ' ') + '%')
$('<div></div>').addClass('meta').html(total + '/' + full + ' [' + formatter.format(Math.round(total / full * 10000) / 100).padStart(2, ' ') + '%]')
)
)
})

View File

@ -2,7 +2,7 @@
<div class="ui divided list" id="cierres_vigentes"></div>
@push('page_scripts')
<script type="text/javascript">
<script>
const cierres_vigentes = {
get: function() {
const list = $('#cierres_vigentes')

View File

@ -2,7 +2,7 @@
<div class="ui divided list" id="cuotas_por_vencer"></div>
@push('page_scripts')
<script type="text/javascript">
<script>
const cuotas_por_vencer = {
get: function() {
const list = $('#cuotas_por_vencer')

View File

@ -10,11 +10,6 @@
<div class="column">
Inmobiliarias
</div>
{{--<div class="right aligned column">
<button class="ui icon button" type="button">
<i class="plus icon"></i>
</button>
</div>--}}
</h2>
<div class="ui divider"></div>
<div class="ui cards">
@ -22,11 +17,9 @@
<div class="ui card">
<div class="content">
<div class="header">
{{$inmobiliaria->abreviacion}}
{{--<a href="{{$urls->base}}/inmobiliaria/{{$inmobiliaria->rut}}">
</a>--}}
{{$inmobiliaria->abreviacion}}
</div>
<div class="description">{{$inmobiliaria->razon}} {{$inmobiliaria->tipoSociedad->descripcion}}</div>
<div class="description">{{$inmobiliaria->razon}} {{$inmobiliaria->tipoSociedad?->descripcion ?? ''}}</div>
<div class="meta">{{$inmobiliaria->rut()}}</div>
</div>
</div>

View File

@ -0,0 +1,171 @@
@extends('layout.base')
@section('page_content')
<div class="ui container">
<table class="ui table">
<thead>
<tr>
<th>Nombre</th>
<th>Contacto</th>
<th class="right aligned">
<button class="ui tertiary green icon button" id="add_button">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
<tbody id="proveedores">
@foreach ($proveedores as $proveedor)
<tr>
<td>{{$proveedor->nombre}}</td>
<td>{{$proveedor->contacto?->nombreCompleto()}}</td>
<td class="right aligned">
<button class="ui tertiary icon button" data-proveedor="{{$proveedor->rut}}">
<i class="edit icon"></i>
</button>
<button class="ui tertiary red icon button" data-proveedor="{{$proveedor->rut}}">
<i class="remove icon"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@include('inmobiliarias.proveedores.add_modal')
@include('inmobiliarias.proveedores.edit_modal')
@endsection
@include('layout.body.scripts.rut')
@push('page_scripts')
<script>
const proveedores = {
ids: {
buttons: {
add: '',
edit: '',
remove: ''
},
add: {
form: '',
rut: '',
dv: '',
nombre: '',
razon: '',
contacto: {
rut: '',
dv: '',
nombre: '',
apellido_paterno: '',
apellido_materno: '',
email: '',
telefono: ''
}
},
proveedores: ''
},
data: JSON.parse('{!! json_encode($proveedores) !!}'),
remove() {
return {
proveedor: rut => {
const url = `{{$urls->api}}/inmobiliarias/proveedor/${rut}/delete`
const method = 'delete'
return APIClient.fetch(url, {method})
.then(response => (response) ? response.json() : null)
.then(data => {
if (data.success) {
window.location.reload()
}
})
}
}
},
formatters() {
return {
rut: value => {
return Rut.format(value)
},
telefono: value => {
const phone = value.replace(/[^0-9]/g, '')
if (phone.length <= 1) {
return phone
}
return phone.replace(/(\d{2})(\d{3})(\d{4})/, '$1 $2 $3')
}
}
},
setup(ids) {
this.ids = ids
const addModal = new AddModal(this.ids.add, this.formatters())
$(this.ids.buttons.add).click(() => {
addModal.show()
})
const editModal = new EditModal(this.ids.edit, this.formatters())
$(this.ids.buttons.edit).click((e) => {
e.currentTarget.classList.add('loading')
e.currentTarget.classList.add('spinner')
editModal.show(e.currentTarget.parentNode.dataset.proveedor).then(() => {
e.currentTarget.classList.remove('loading')
e.currentTarget.classList.remove('spinner')
})
})
$(this.ids.buttons.remove).click((e) => {
e.currentTarget.classList.add('loading')
e.currentTarget.classList.add('spinner')
this.remove().proveedor(e.currentTarget.parentNode.dataset.proveedor).then(() => {
e.currentTarget.classList.remove('loading')
e.currentTarget.classList.remove('spinner')
})
})
}
}
$(document).ready(() => {
proveedores.setup({
buttons: {
add: '#add_button',
edit: '.edit',
remove: '.remove'
},
add: {
modal: '#add_modal',
form: 'add_form',
rut: '#rut',
digito: '#dv',
nombre: '#nombre',
razon: '#razon',
contacto: {
rut: '#rut_contacto',
digito: '#dv_contacto',
nombres: '#nombre_contacto',
apellido_paterno: '#apellido_paterno_contacto',
apellido_materno: '#apellido_materno_contacto',
email: '#email_contacto',
telefono: '#telefono_contacto'
}
},
edit: {
modal: '#edit_modal',
form: 'edit_form',
rut: '#edit_rut',
digito: '#edit_digito',
nombre: '#edit_nombre',
razon: '#edit_razon',
contacto: {
rut: '#edit_rut_contacto',
digito: '#edit_digito_contacto',
nombres: '#edit_nombre_contacto',
apellido_paterno: '#edit_apellido_paterno_contacto',
apellido_materno: '#edit_apellido_materno_contacto',
email: '#edit_email_contacto',
telefono: '#edit_telefono_contacto'
}
},
proveedores: '#proveedores'
})
})
</script>
@endpush

View File

@ -0,0 +1,137 @@
<div class="ui modal" id="add_modal">
<div class="header">Agregar Proveedor</div>
<div class="content">
<form class="ui form" id="add_form">
<div class="three wide field">
<label for="rut">RUT</label>
<div class="ui right labeled input">
<input class="right aligned" type="text" id="rut" name="rut" placeholder="RUT" maxlength="10"
required/>
<div class="ui basic label">-<span id="dv"></span></div>
</div>
</div>
<div class="five wide field">
<label for="nombre">Nombre</label>
<input type="text" id="nombre" name="nombre" placeholder="Nombre" required/>
</div>
<div class="field">
<label for="razon">Razón Social</label>
<input type="text" id="razon" name="razon" placeholder="Razón Social" required/>
</div>
<div class="ui divider">Contacto</div>
<div class="three wide field">
<label for="rut_contacto">RUT</label>
<div class="ui right labeled input">
<input type="text" id="rut_contacto" name="rut_contacto" placeholder="RUT" maxlength="10"/>
<div class="ui basic label">-<span id="dv_contacto"></span></div>
</div>
</div>
<div class="fields">
<div class="five wide field">
<label for="nombre_contacto">Nombre</label>
<input type="text" id="nombre_contacto" name="nombre_contacto" placeholder="Nombre" />
</div>
<div class="field">
<label for="apellido_paterno_contacto">Apellido Paterno</label>
<input type="text" id="apellido_paterno_contacto" name="apellido_paterno_contacto"
placeholder="Apellido Paterno"/>
</div>
<div class="field">
<label for="apellido_materno_contacto">Apellido Materno</label>
<input type="text" id="apellido_materno_contacto" name="apellido_materno_contacto"
placeholder="Apellido Materno"/>
</div>
</div>
<div class="field">
<label for="email_contacto">Email</label>
<input type="email" id="email_contacto" name="email_contacto" placeholder="Email"/>
</div>
<div class="field">
<label for="telefono_contacto">Teléfono</label>
<div class="ui left labeled input">
<div class="ui basic label">+56</div>
<input type="text" id="telefono_contacto" name="telefono_contacto" placeholder="Teléfono"/>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui green approve button">Guardar</button>
</div>
</div>
@push('page_scripts')
<script>
class AddModal {
props
constructor(props, formatters) {
this.props = props
this.setup(formatters)
}
show() {
$(this.props.modal).modal('show')
}
add() {
const form = document.getElementById(this.props.form)
const data = {
rut: form.querySelector(this.props.rut).value.replace(/\D/g, ''),
digito: form.querySelector(this.props.digito).textContent,
nombre: form.querySelector(this.props.nombre).value,
razon: form.querySelector(this.props.razon).value,
contacto: {
rut: form.querySelector(this.props.contacto.rut).value.replace(/\D/g, ''),
digito: form.querySelector(this.props.contacto.digito).textContent,
nombres: form.querySelector(this.props.contacto.nombres).value,
apellido_paterno: form.querySelector(this.props.contacto.apellido_paterno).value,
apellido_materno: form.querySelector(this.props.contacto.apellido_materno).value,
email: form.querySelector(this.props.contacto.email).value,
telefono: form.querySelector(this.props.contacto.telefono).value.replace(/\D/g, ''),
}
}
const body = new FormData()
body.append('proveedores[]', JSON.stringify(data))
const url = '{{$urls->api}}/inmobiliarias/proveedores/add'
const method = 'post'
APIClient.fetch(url, {method, body})
.then(response => (response) ? response.json() : null)
.then(data => {
if (data.success.filter(s => s).length > 0) {
window.location.reload()
}
})
}
setup(formatters) {
$(this.props.modal).modal({
onApprove: () => {
this.add()
}
})
const form = document.getElementById(this.props.form)
form.querySelector(this.props.rut).addEventListener('input', (e) => {
e.currentTarget.value = formatters.rut(e.currentTarget.value)
form.querySelector(this.props.digito).textContent = Rut.digitoVerificador(e.currentTarget.value)
})
if (form.querySelector(this.props.rut).value.length > 0) {
form.querySelector(this.props.rut).value = formatters.rut(form.querySelector(this.props.rut).value)
form.querySelector(this.props.digito).textContent = Rut.digitoVerificador(form.querySelector(this.props.rut).value)
}
form.querySelector(this.props.contacto.rut).addEventListener('input', (e) => {
e.currentTarget.value = formatters.rut(e.currentTarget.value)
form.querySelector(this.props.contacto.digito).textContent = Rut.digitoVerificador(e.currentTarget.value)
})
if (form.querySelector(this.props.contacto.rut).value.length > 0) {
form.querySelector(this.props.contacto.rut).value = formatters.rut(form.querySelector(this.props.contacto.rut).value)
form.querySelector(this.props.contacto.digito).textContent = Rut.digitoVerificador(form.querySelector(this.props.contacto.rut).value)
}
form.querySelector(this.props.contacto.telefono).addEventListener('input', (e) => {
e.currentTarget.value = formatters.telefono(e.currentTarget.value)
})
if (form.querySelector(this.props.contacto.telefono).value.length > 0) {
form.querySelector(this.props.contacto.telefono).value = formatters.telefono(form.querySelector(this.props.contacto.telefono).value)
}
}
}
</script>
@endpush

View File

@ -0,0 +1,157 @@
<div class="ui modal" id="edit_modal">
<div class="header">Editar Proveedor <span class="proveedor"></span></div>
<div class="content">
<form class="ui form" id="edit_form">
<div class="three wide field">
<label for="edit_rut">RUT</label>
<div class="ui right labeled input">
<input class="right aligned" type="text" id="edit_rut" name="rut" placeholder="RUT" maxlength="10"
required/>
<div class="ui basic label">-<span id="edit_digito"></span></div>
</div>
</div>
<div class="five wide field">
<label for="edit_nombre">Nombre</label>
<input type="text" id="edit_nombre" name="nombre" placeholder="Nombre" required/>
</div>
<div class="field">
<label for="edit_razon">Razón Social</label>
<input type="text" id="edit_razon" name="razon" placeholder="Razón Social" required/>
</div>
<div class="ui divider">Contacto</div>
<div class="three wide field">
<label for="edit_rut_contacto">RUT</label>
<div class="ui right labeled input">
<input type="text" id="edit_rut_contacto" name="rut_contacto" placeholder="RUT" maxlength="10" />
<div class="ui basic label">-<span id="edit_digito_contacto"></span></div>
</div>
</div>
<div class="fields">
<div class="five wide field">
<label for="edit_nombre_contacto">Nombre</label>
<input type="text" id="edit_nombre_contacto" name="nombre_contacto" placeholder="Nombre" />
</div>
<div class="field">
<label for="edit_apellido_paterno_contacto">Apellido Paterno</label>
<input type="text" id="edit_apellido_paterno_contacto" name="apellido_paterno_contacto"
placeholder="Apellido Paterno" />
</div>
<div class="field">
<label for="edit_apellido_materno_contacto">Apellido Materno</label>
<input type="text" id="edit_apellido_materno_contacto" name="apellido_materno_contacto"
placeholder="Apellido Materno" />
</div>
</div>
<div class="field">
<label for="edit_email_contacto">Email</label>
<input type="email" id="edit_email_contacto" name="email_contacto" placeholder="Email"/>
</div>
<div class="field">
<label for="edit_telefono_contacto">Teléfono</label>
<div class="ui left labeled input">
<div class="ui basic label">+56</div>
<input type="text" id="edit_telefono_contacto" name="telefono_contacto" placeholder="Teléfono"/>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui green approve button">Guardar</button>
</div>
</div>
@push('page_scripts')
<script>
class EditModal {
props
data
constructor(props, formatter) {
this.props = props
this.setup(formatter)
}
show(id) {
return this.load(id).then(() => {
$(this.props.modal).find('.proveedor').html(this.data.nombre)
const form = document.getElementById(this.props.form)
form.querySelector(this.props.rut).value = this.data.rut
form.querySelector(this.props.digito).textContent = Rut.digitoVerificador(this.data.rut)
form.querySelector(this.props.nombre).value = this.data.nombre
form.querySelector(this.props.razon).value = this.data.razon
form.querySelector(this.props.contacto.rut).value = (this.data.contacto) ? this.data.contacto.rut : ''
form.querySelector(this.props.contacto.digito).textContent = (this.data.contact) ? Rut.digitoVerificador(this.data.contacto.rut) : ''
form.querySelector(this.props.contacto.nombres).value = (this.data.contact) ? this.data.contacto.nombres : ''
form.querySelector(this.props.contacto.apellido_paterno).value = (this.data.contact) ? this.data.contacto.apellidoPaterno : ''
form.querySelector(this.props.contacto.apellido_materno).value = (this.data.contact) ? this.data.contacto.apellidoMaterno : ''
form.querySelector(this.props.contacto.email).value = (this.data.contact) ? this.data.contacto.datos.email : ''
form.querySelector(this.props.contacto.telefono).value = (this.data.contact) ? this.data.contacto.datos.telefono : ''
$(this.props.modal).modal('show')
})
}
load(id) {
const url = `{{$urls->api}}/inmobiliarias/proveedor/${id}`
return APIClient.fetch(url).then(response => response.json()).then(json => {
this.data = json.proveedor
})
}
edit() {
const form = document.getElementById(this.props.form)
let data = {
rut: form.querySelector(this.props.rut).value.replace(/\D/g, ''),
digito: form.querySelector(this.props.digito).textContent,
nombre: form.querySelector(this.props.nombre).value,
razon: form.querySelector(this.props.razon).value,
contacto: {
rut: form.querySelector(this.props.contacto.rut).value.replace(/\D/g, ''),
digito: form.querySelector(this.props.contacto.digito).textContent,
nombres: form.querySelector(this.props.contacto.nombres).value,
apellido_paterno: form.querySelector(this.props.contacto.apellido_paterno).value,
apellido_materno: form.querySelector(this.props.contacto.apellido_materno).value,
email: form.querySelector(this.props.contacto.email).value,
telefono: form.querySelector(this.props.contacto.telefono).value.replace(/\D/g, ''),
}
}
const body = new FormData()
body.append('proveedores[]', JSON.stringify(data))
const url = '{{$urls->api}}/inmobiliarias/proveedores/edit'
const method = 'post'
return APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (json.success.filter(s => s).length > 0) {
window.location.reload()
}
})
}
setup(formatter) {
$(this.props.modal).modal({
onApprove: () => {
this.edit()
}
})
const form = document.getElementById(this.props.form)
form.querySelector(this.props.rut).addEventListener('input', (e) => {
e.currentTarget.value = formatter.rut(e.currentTarget.value)
form.querySelector(this.props.digito).textContent = Rut.digitoVerificador(e.currentTarget.value)
})
if (form.querySelector(this.props.rut).value.length > 0) {
form.querySelector(this.props.rut).value = formatter.rut(form.querySelector(this.props.rut).value)
form.querySelector(this.props.digito).textContent = Rut.digitoVerificador(form.querySelector(this.props.rut).value)
}
form.querySelector(this.props.contacto.rut).addEventListener('input', (e) => {
e.currentTarget.value = formatter.rut(e.currentTarget.value)
form.querySelector(this.props.contacto.digito).textContent = Rut.digitoVerificador(e.currentTarget.value)
})
if (form.querySelector(this.props.contacto.rut).value.length > 0) {
form.querySelector(this.props.contacto.rut).value = formatter.rut(form.querySelector(this.props.contacto.rut).value)
form.querySelector(this.props.contacto.digito).textContent = Rut.digitoVerificador(form.querySelector(this.props.contacto.rut).value)
}
form.querySelector(this.props.contacto.telefono).addEventListener('input', (e) => {
e.currentTarget.value = formatter.telefono(e.currentTarget.value)
})
if (form.querySelector(this.props.contacto.telefono).value.length > 0) {
form.querySelector(this.props.contacto.telefono).value = formatter.telefono(form.querySelector(this.props.contacto.telefono).value)
}
}
}
</script>
@endpush

View File

@ -16,7 +16,14 @@
<a class="item" href="{{$urls->base}}/contabilidad/centros_costos/asignar">Asignar en Cartola</a>
</div>
</div>
<a class="item" href="{{$urls->base}}/contabilidad/cartolas/diaria">Cartola Diaria</a>
<div class="item">
<i class="dropdown icon"></i>
<a class="text" href="{{$urls->base}}/contabilidad/cartolas/diaria">Cartola Diaria</a>
<div class="menu">
<a class="item" href="{{$urls->base}}/contabilidad/cartolas/importar">Importar</a>
</div>
</div>
<a class="item" href="{{$urls->base}}/contabilidad/depositos">Depósitos a Plazo</a>
<a class="item" href="{{$urls->base}}/contabilidad/movimientos">Movimientos</a>
</div>
</div>

View File

@ -1 +1,7 @@
<a class="item" href="{{$urls->base}}/inmobiliarias">Inmobiliarias</a>
<div class="ui simple dropdown item">
<a class="text" href="{{$urls->base}}/inmobiliarias">Inmobiliarias</a>
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/inmobiliarias/proveedores">Proveedores</a>
</div>
</div>

View File

@ -1,8 +1,8 @@
<div class="ui simple dropdown item">
Proyectos
<a class="text" href="{{$urls->base}}/proyectos">Proyectos</a>
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/proyectos">Listado</a>
<a class="item" href="{{$urls->base}}/proyectos/unidades">Unidades</a>
<a class="item" href="{{ $urls->base }}/proyectos/brokers">Operadores</a>
</div>
</div>

View File

@ -11,7 +11,7 @@
</div>
@push('page_scripts')
<script type="text/javascript">
<script>
function logout() {
return fetch('{{$urls->base}}/logout').then(response => {
if (response.ok) {

View File

@ -1,26 +1,26 @@
<div class="ui simple dropdown item">
Ventas
<a class="text" href="{{$urls->base}}/ventas">Ventas</a>
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/ventas/precios">Precios</a>
<a class="item" href="{{ $urls->base }}/ventas/promotions">Promociones</a>
<a class="item" href="{{$urls->base}}/ventas/cierres">Cierres</a>
<div class="item">
Cuotas
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/ventas/cuotas/pendientes">Pendientes</a>
<a class="item" href="{{$urls->base}}/ventas/cuotas/abonar">Abonar</a>
</div>
</div>
{{--<div class="item">
Listados
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/ventas/precios">Precios</a>
<a class="item" href="{{$urls->base}}/ventas/cierres">Cierres</a>
<a class="item" href="{{$urls->base}}/ventas/">Ventas</a>
<div class="item">
Cuotas
<i class="dropdown icon"></i>
<div class="menu">
<a class="item" href="{{$urls->base}}/ventas/cuotas/pendientes">Pendientes</a>
<a class="item" href="{{$urls->base}}/ventas/cuotas/abonar">Abonar</a>
</div>
</div>
{{--<a class="item" href="{{$urls->base}}/ventas/pagos/pendientes">Pagos Pendientes</a>--}}
{{--<a class="item" href="{{$urls->base}}/ventas/consolidado">Consolidado Ventas</a>--}}
--}}{{--<a class="item" href="{{$urls->base}}/ventas/pagos/pendientes">Pagos Pendientes</a>--}}{{--
--}}{{--<a class="item" href="{{$urls->base}}/ventas/consolidado">Consolidado Ventas</a>--}}{{--
</div>
</div>
</div>--}}
{{--<div class="item">
Informes
<i class="dropdown icon"></i>
@ -33,6 +33,8 @@
</div>--}}
{{--<a class="item" href="{{$urls->base}}/ventas/precios/importar">Importar Precios</a>--}}
{{--<a class="item" href="{{$urls->base}}/ventas/cierres/evaluar">Evaluar Cierre</a>--}}
<a class="item" href="{{$urls->base}}/ventas/facturacion">Facturación</a>
<div class="divider"></div>
<a class="item" href="{{$urls->base}}/ventas/add">
Nueva Venta
<i class="plus icon"></i>

View File

@ -1,25 +1,30 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js" integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fomantic-ui/2.9.3/semantic.min.js" integrity="sha512-gnoBksrDbaMnlE0rhhkcx3iwzvgBGz6mOEj4/Y5ZY09n55dYddx6+WYc72A55qEesV8VX2iMomteIwobeGK1BQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script type="text/javascript">
function fetchAPI(url, options=null) {
if (options === null) {
options = {}
}
if (!Object.hasOwn(options, 'headers')) {
options['headers'] = {}
}
if (!Object.hasOwn(options['headers'], 'Authorization')) {
options['headers']['Authorization'] = 'Bearer {{md5($API_KEY)}}'
}
return fetch(url, options).then(response => {
if (response.ok) {
return response
@include('layout.body.scripts.api')
<script>
const datatables_defaults = {
language: {
emptyTable: 'No hay datos disponibles',
info: 'Mostrando _START_ a _END_ de _TOTAL_ registros',
infoEmpty: 'Mostrando desde 0 a 0 de 0 registros',
infoFiltered: '(filtrado de _MAX_ registros totales)',
lengthMenu: 'Mostrar _MENU_ registros',
loadingRecords: 'Cargando...',
search: 'Buscar:',
zeroRecords: 'No se encontraron registros',
paginate: {
first: 'Primero',
last: 'Último',
next: 'Siguiente',
previous: 'Anterior',
},
aria: {
orderable: 'Ordenar por esta columna',
orderableReverse: 'Ordenar por esta columna en orden inverso',
}
throw new Error(JSON.stringify({code: response.status, message: response.statusText, url}))
}).catch(error => {
console.error(error)
})
}
}
const calendar_date_options = {
type: 'date',
@ -28,9 +33,12 @@
monthFirst: false,
text: {
days: ['Do', 'Lu', 'Ma', 'Mi', 'Ju', 'Vi', 'Sa'],
dayNamesShort: ['Dom', 'Lun', 'Mar', 'Mie', 'Jue', 'Vie', 'Sab'],
dayNames: ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'],
months: ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'],
monthsShort: ['Ene', 'Feb', 'Mar', 'Abr', 'May', 'Jun', 'Jul', 'Ago', 'Sep', 'Oct', 'Nov', 'Dic'],
today: 'Hoy'
today: 'Hoy',
now: 'Ahora',
},
formatter: {
date: 'DD-MM-YYYY'

View File

@ -0,0 +1,32 @@
<script>
class APIClient {
static getApiKey() {
return '{{md5($API_KEY)}}{{($login->isIn()) ? $login->getSeparator() . $login->getToken() : ''}}'
}
static fetch(url, options=null, showErrors=false) {
if (options === null) {
options = {}
}
if (!Object.hasOwn(options, 'headers')) {
options['headers'] = {}
}
if (!Object.hasOwn(options['headers'], 'Authorization')) {
options['headers']['Authorization'] = `Bearer ${APIClient.getApiKey()}`
}
return fetch(url, options).then(response => {
if (response.ok) {
return response
}
throw new Error(JSON.stringify({code: response.status, message: response.statusText, url}))
}).catch(error => {
if (showErrors) {
console.error(error)
}
})
}
}
function fetchAPI(url, options=null, showErrors=false) {
return APIClient.fetch(url, options, showErrors)
}
</script>

View File

@ -0,0 +1,3 @@
@push('page_scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.2.0/crypto-js.min.js" integrity="sha512-a+SUDuwNzXDvz4XrIcXHuCf089/iJAoN4lmrXJg18XnduKK6YlDHNRalv4yd1N40OKI80tFidF+rqTFKGPoWFQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
@endpush

View File

@ -1,5 +1,4 @@
@push('page_scripts')
{{--<script type="text/javascript" src="https://cdn.datatables.net/2.0.1/js/jquery.dataTables.min.js"></script>--}}
<script type="text/javascript" src="https://cdn.datatables.net/2.0.1/js/dataTables.min.js"></script>
<script src="https://cdn.datatables.net/2.0.1/js/dataTables.semanticui.min.js"></script>
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.min.js"></script>
<script src="https://cdn.datatables.net/2.0.3/js/dataTables.semanticui.min.js"></script>
@endpush

View File

@ -1,9 +1,9 @@
@push('page_scripts')
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfmake/0.2.9/pdfmake.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdn.datatables.net/buttons/3.0.0/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.0/js/buttons.semanticui.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.0/js/buttons.colVis.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.0/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.0/js/buttons.print.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.1/js/dataTables.buttons.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.1/js/buttons.semanticui.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.1/js/buttons.colVis.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.1/js/buttons.html5.min.js"></script>
<script src="https://cdn.datatables.net/buttons/3.0.1/js/buttons.print.min.js"></script>
@endpush

View File

@ -0,0 +1,27 @@
@push('page_scripts')
<script src="https://cdn.datatables.net/datetime/1.5.2/js/dataTables.dateTime.min.js"></script>
<script src="https://cdn.datatables.net/searchbuilder/1.7.0/js/dataTables.searchBuilder.min.js"></script>
<script src="https://cdn.datatables.net/searchbuilder/1.7.0/js/searchBuilder.semanticui.js"></script>
<script>
const searchBuilder = {
add: 'Filtrar',
condition: 'Comparador',
clearAll: 'Resetear',
delete: 'Eliminar',
deleteTitle: 'Eliminar Titulo',
data: 'Columna',
left: 'Izquierda',
leftTitle: 'Titulo Izquierdo',
logicAnd: 'Y',
logicOr: 'O',
right: 'Derecha',
rightTitle: 'Titulo Derecho',
title: {
0: 'Filtros',
_: 'Filtros (%d)'
},
value: 'Opciones',
valueJoiner: 'y'
}
</script>
@endpush

View File

@ -0,0 +1,3 @@
@push('page_scripts')
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4/build/global/luxon.min.js" integrity="sha256-7NQm0bhvDJKosL8d+6ZgSi2LxZCIcA/TD087GLEBO9M=" crossorigin="anonymous"></script>
@endpush

View File

@ -0,0 +1,45 @@
@push('page_scripts')
<script>
Intl.NumberFormat.prototype.parse = function(valueString) {
const format = new Intl.NumberFormat(this.resolvedOptions().locale);
const parts = format.formatToParts(-12345.6);
const numerals = Array.from({ length: 10 }).map((_, i) => format.format(i));
const index = new Map(numerals.map((d, i) => [d, i]));
_minusSign = new RegExp(`[${parts.find(d => d.type === 'minusSign').value}]`);
_group = new RegExp(`[${parts.find(d => d.type === 'group').value}]`, 'g');
_decimal = new RegExp(`[${parts.find(d => d.type === 'decimal').value}]`);
_numeral = new RegExp(`[${numerals.join('')}]`, 'g');
_index = d => index.get(d);
const DIRECTION_MARK = /\u061c|\u200e/g
return +(
valueString.trim()
.replace(DIRECTION_MARK, '')
.replace(_group, '')
.replace(_decimal, '.')
.replace(_numeral, _index)
.replace(_minusSign, '-')
)
}
Intl.NumberFormat.prototype.isLocale = function(stringValue) {
const format = new Intl.NumberFormat(this.resolvedOptions().locale);
const parts = format.formatToParts(-12345.6);
const group = parts.find(d => d.type === 'group').value;
const decimal = parts.find(d => d.type === 'decimal').value;
if (stringValue.includes(group)) {
if (stringValue.includes(decimal)) {
return stringValue.indexOf(group) < stringValue.indexOf(decimal)
}
if (stringValue.split(group).map(d => d.length).filter(d => d > 3).length > 0) {
return false
}
return stringValue.split(group).length > 2;
}
if (stringValue.includes(decimal)) {
return stringValue.split(decimal).length <= 2;
}
return false
}
</script>
@endpush

View File

@ -0,0 +1,76 @@
@push('page_scripts')
<script>
if (typeof Intl.NumberFormat.isLocale === 'undefined' || typeof Intl.NumberFormat.isLocale !== 'function') {
// Load Intl.NumberFormat custom methods
@include('layout.body.scripts.number_format')
}
class NumberInput {
input
isRational
outputLocale
currentValue
formatters
constructor({input, isRational, outputLocale}) {
this.input = input
this.isRational = isRational
this.outputLocale = outputLocale || 'es-CL'
this.formatters = {}
const locales = ['es-CL', 'en-US']
locales.forEach(locale => {
this.formatters[locale] = {
rational: new Intl.NumberFormat(locale, {minimumFractionDigits: 2, maximumFractionDigits: 2}),
integer: new Intl.NumberFormat(locale)
}
})
if (this.input.value !== '') {
this.currentValue = this.process(this.input.value)
this.input.value = this.format(this.currentValue)
}
}
watch() {
this.input.addEventListener('change', event => {
this.currentValue = this.process(event.currentTarget.value)
this.input.value = this.format(this.currentValue)
})
}
process(stringValue) {
if (stringValue === '') {
return ''
}
if (typeof stringValue !== 'string') {
return stringValue
}
return this.formatters[this.detectLocale(stringValue)][this.isRational ? 'rational' : 'integer'].parse(stringValue)
}
detectLocale(stringValue) {
if (stringValue === '') {
return ''
}
if (typeof stringValue !== 'string') {
return stringValue
}
const outputFormat = this.formatters[this.outputLocale][this.isRational ? 'rational' : 'integer'].isLocale(stringValue)
const otherFormats = Object.entries(this.formatters).filter(formatter => formatter[0] !== this.outputLocale).map(formatter => {
return {
locale: formatter[0],
value: formatter[1][this.isRational ? 'rational' : 'integer'].isLocale(stringValue)
}
}).filter(formatter => formatter.value)
if (outputFormat) {
return this.outputLocale
}
if (otherFormats.length > 0) {
return otherFormats[0].locale
}
return 'en-US'
}
format(value) {
return this.formatters[this.outputLocale][this.isRational ? 'rational' : 'integer'].format(value)
}
}
</script>
@endpush

View File

@ -0,0 +1,41 @@
@push('page_scripts')
<script>
class Rut {
static digitoVerificador(rut) {
if (!(typeof rut === 'string' || rut instanceof String)) {
rut = rut.toString()
}
if (rut.length === 0) {
return ''
}
let M = 0, S = 1
for (; rut; rut = Math.floor(rut / 10)) {
S = (S + rut % 10 * (9 - M++ % 6)) % 11
}
return S ? S - 1 : 'K'
}
static format(rut) {
if (!(typeof rut === 'string' || rut instanceof String)) {
rut = rut.toString()
}
if (rut.length === 0) {
return ''
}
rut.replace(/\D/g, '')
return rut.replace(/^(\d{1,2})(\d{3})(\d{3})$/, '$1.$2.$3')
}
static clean(rut) {
if (!(typeof rut === 'string' || rut instanceof String)) {
rut = rut.toString()
}
return rut.replace(/\D/g, '')
}
static validar(rut, digito) {
if (!(typeof digito === 'string' || digito instanceof String)) {
digito = digito.toString()
}
return Rut.digitoVerificador(rut).toString().toUpperCase() === digito.toUpperCase()
}
}
</script>
@endpush

View File

@ -0,0 +1,6 @@
@prepend('page_scripts')
<script src='https://unpkg.com/simple-statistics@7.8.8/dist/simple-statistics.min.js'></script>
<script>
const Stat = ss
</script>
@endprepend

View File

@ -1,9 +1,9 @@
<head>
<meta charset="utf8" />
@hasSection('page_title')
<title>Incoviba - @yield('page_title')</title>
<title>Incoviba - @yield('page_title')</title>
@else
<title>Incoviba</title>
<title>Incoviba</title>
@endif
<link rel="icon" href="{{$urls->images}}/Isotipo 16.png" />
@include('layout.head.styles')

View File

@ -1,3 +1,3 @@
@push('page_styles')
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.1/css/dataTables.semanticui.min.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/2.0.3/css/dataTables.semanticui.min.css" />
@endpush

View File

@ -0,0 +1,4 @@
@push('page_styles')
<link rel="stylesheet" href="https://cdn.datatables.net/datetime/1.5.2/css/dataTables.dateTime.min.css" />
<link rel="stylesheet" href="https://cdn.datatables.net/searchbuilder/1.7.0/css/searchBuilder.dataTables.min.css" />
@endpush

View File

@ -16,29 +16,30 @@
</div>
@endsection
@include('layout.body.scripts.cryptojs')
@push('page_scripts')
<script type="text/javascript">
<script>
function encryptPassword(password) {
const passphrase = Math.floor(Math.random() * Date.now()).toString()
const encrypted = CryptoJS.AES.encrypt(password, passphrase)
return [passphrase, encrypted.toString()].join('')
}
function sendLogin(name, password) {
const data = new FormData()
data.append('name', name)
data.append('password', password)
return fetch('{{$urls->base}}/login', {
method: 'post',
headers: {
Accept: 'json'
},
body: data
}).then(response => {
const method = 'post'
const headers = {
Accept: 'json'
}
const body = new FormData()
body.append('name', name)
body.append('password', encryptPassword(password))
return fetch('{{$urls->base}}/login', {method, headers, body}).then(response => {
if (response.ok) {
return response.json()
}
}).then(data => {
if (data.login === true) {
@if(isset($redirect_uri))
window.location = '{{$redirect_uri}}'
@else
window.location = '{{$urls->base}}'
@endif
window.location = '{{(isset($redirect_uri)) ? $redirect_uri : $urls->base}}'
}
})
}

View File

@ -0,0 +1,14 @@
@extends('layout.base')
@section('page_title')
405 - Método No Permitido
@endsection
@section('page_content')
<div class="ui container">
<div class="ui message">
<i class="exclamation triangle icon"></i>
No se ha encontrado la página solicitada para el método solicitado.
</div>
</div>
@endsection

View File

@ -0,0 +1,219 @@
@extends('proyectos.brokers.base')
@section('brokers_content')
<table id="brokers" class="ui table">
<thead>
<tr>
<th>RUT</th>
<th>Nombre</th>
<th>Contacto</th>
<th>Contratos</th>
<th class="right aligned">
<button class="ui small tertiary green icon button" id="add_button">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
@foreach($brokers as $broker)
<tr>
<td class="top aligned">
<span
@if ($broker->data()?->legalName !== '')
data-tooltip="{{ $broker->data()?->legalName }}" data-position="right center"
@endif
>
{{$broker->rutFull()}}
</span>
</td>
<td class="top aligned">
<a href="{{ $urls->base }}/proyectos/broker/{{ $broker->rut }}">
{{$broker->name}}
<i class="angle right icon"></i>
</a>
</td>
<td class="top aligned">
<span
@if ($broker->data()?->representative?->email !== '' || $broker->data()?->representative?->phone !== '')
data-tooltip="{{ ($broker->data()?->representative?->email !== '') ? "Email: " . $broker->data()?->representative?->email : '' }}{{ ($broker->data()?->representative?->phone !== '') ? ' Teléfono: ' . $broker->data()?->representative?->phone : '' }}" data-position="right center"
@endif
>
{{ $broker->data()?->representative?->name }}
</span>
</td>
<td class="top aligned">
<div class="ui list">
@foreach($broker->contracts() as $contract)
<div class="item">
<a href="{{$urls->base}}/proyectos/broker/{{$broker->rut}}/contract/{{$contract->id}}" data-tooltip="{{$contract->current()->date->format('d-m-Y')}}" data-position="right center">
{{$contract->project->descripcion}} ({{$format->percent($contract->commission, 2, true)}})
</a>
</div>
@endforeach
</div>
</td>
<td class="top aligned right aligned">
<button class="ui small tertiary icon button edit_button" data-index="{{$broker->rut}}">
<i class="edit icon"></i>
</button>
<button class="ui small tertiary red icon button remove_button" data-index="{{$broker->rut}}">
<i class="remove icon"></i>
</button>
</td>
</tr>
@endforeach
</table>
@include('proyectos.brokers.add_modal')
@include('proyectos.brokers.edit_modal')
@endsection
@push('page_scripts')
<script>
function storeBrokers() {
localStorage.setItem('brokers', '{!! json_encode(array_map(function($broker) {
$arr = json_decode(json_encode($broker), true);
array_walk_recursive($arr, function(&$val, $key) {
if ($val === null) {
$val = '';
}
});
$arr['contracts'] = $broker->contracts();
return $arr;
}, $brokers)) !!}')
}
const brokersHandler = {
ids: {
buttons: {
add: 'add_button',
edit: 'edit_button',
remove: 'remove_button'
},
modals: {
add: '',
edit: ''
},
forms: {
add: '',
edit: ''
}
},
modals: {
add: null,
edit: null
},
events() {
return {
add: clickEvent => {
clickEvent.preventDefault()
brokersHandler.actions().add()
},
edit: clickEvent => {
clickEvent.preventDefault()
const broker_rut = parseInt(clickEvent.currentTarget.dataset.index)
brokersHandler.actions().edit(broker_rut)
},
delete: clickEvent => {
clickEvent.preventDefault()
const broker_rut = clickEvent.currentTarget.dataset.index
brokersHandler.actions().delete(broker_rut)
}
}
},
buttonWatch() {
document.getElementById(brokersHandler.ids.buttons.add).addEventListener('click', brokersHandler.events().add)
Array.from(document.getElementsByClassName(brokersHandler.ids.buttons.edit)).forEach(button => {
button.addEventListener('click', brokersHandler.events().edit)
})
Array.from(document.getElementsByClassName(brokersHandler.ids.buttons.remove)).forEach(button => {
button.addEventListener('click', brokersHandler.events().delete)
})
},
actions() {
return {
add: () => {
this.modals.add.show()
},
edit: broker_rut => {
const localData = JSON.parse(localStorage.getItem('brokers'))
const broker = localData.find(broker => broker.rut === broker_rut)
const data = {
rut: broker_rut,
name: broker.name,
legal_name: broker.data?.legal_name || '',
contact: broker.data?.representative?.name || '',
email: broker.data?.representative?.email || '',
phone: broker.data?.representative?.phone || '',
address: broker.data?.representative?.address || '',
contracts: broker.contracts
}
this.modals.edit.load(data)
},
delete: broker_rut => {
brokersHandler.execute().delete(broker_rut)
}
}
},
execute() {
return {
add: data => {
const url = '{{$urls->api}}/proyectos/brokers/add'
const method = 'post'
const body = new FormData()
body.append('brokers[]', JSON.stringify(data))
return APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo agregar operador.')
return
}
window.location.reload()
})
},
edit: data => {
const url = '{{$urls->api}}/proyectos/brokers/edit'
const method = 'post'
const body = new FormData()
body.append('brokers[]', JSON.stringify(data))
return APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo editar operador.')
return
}
window.location.reload()
})
},
delete: broker_rut => {
const url = '{{$urls->api}}/proyectos/broker/' + broker_rut
const method = 'delete'
return APIClient.fetch(url, {method}).then(response => response.json()).then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo eliminar operador.')
return
}
window.location.reload()
})
}
}
},
setup(ids) {
brokersHandler.ids = ids
brokersHandler.buttonWatch()
this.modals.add = new AddModal(brokersHandler)
this.modals.edit = new EditModal(brokersHandler)
}
}
$(document).ready(() => {
storeBrokers()
brokersHandler.setup({
buttons: {
add: 'add_button',
edit: 'edit_button',
remove: 'remove_button',
},
})
})
</script>
@endpush

View File

@ -0,0 +1,115 @@
<div class="ui modal" id="add_broker_modal">
<div class="header">
Agregar Operador
</div>
<div class="content">
<form class="ui form" id="add_broker_form">
<div class="fields">
<div class="field">
<label>RUT</label>
<div class="ui right labeled input">
<input type="text" name="rut" placeholder="RUT" maxlength="10" required />
<div class="ui basic label">-<span id="digit"></span></div>
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Nombre</label>
<input type="text" name="name" placeholder="Nombre" required />
</div>
<div class="six wide field">
<label>Razón Social</label>
<input type="text" name="legal_name" placeholder="Razón Social" required />
</div>
</div>
<div class="fields">
<div class="field">
<label>Contacto</label>
<input type="text" name="contact" placeholder="Contacto" />
</div>
</div>
<div class="fields">
<div class="field">
<label>Correo</label>
<input type="email" name="email" placeholder="Correo" />
</div>
<div class="field">
<label>Teléfono</label>
<input type="text" name="phone" placeholder="Teléfono" />
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Agregar
<i class="checkmark icon"></i>
</div>
</div>
</div>
@include('layout.body.scripts.rut')
@push('page_scripts')
<script>
class AddModal {
ids
modal
handler
constructor(handler) {
this.handler = handler
this.ids = {
modal: 'add_broker_modal',
form: 'add_broker_form',
digit: 'digit'
}
this.modal = $(`#${this.ids.modal}`)
this.modal.modal({
onApprove: () => {
const form = document.getElementById(this.ids.form)
const data = {
rut: form.querySelector('[name="rut"]').value.replace(/\D/g, ''),
digit: Rut.digitoVerificador(form.querySelector('[name="rut"]').value),
name: form.querySelector('[name="name"]').value,
legal_name: form.querySelector('[name="legal_name"]').value,
contact: form.querySelector('[name="contact"]').value || '',
email: form.querySelector('[name="email"]').value || '',
phone: form.querySelector('[name="phone"]').value || ''
}
this.handler.execute().add(data)
}
})
this.modal.modal('hide')
const value = document.querySelector(`#${this.ids.form} input[name="rut"]`).value
this.update().digit(value)
this.watch().rut()
}
update() {
return {
digit: value => {
if (value.length > 3) {
document.getElementById(this.ids.digit).textContent = Rut.digitoVerificador(value)
}
}
}
}
watch() {
return {
rut: () => {
document.querySelector(`#${this.ids.form} input[name="rut"]`).addEventListener('input', event => {
const value = event.currentTarget.value
this.update().digit(value)
})
}
}
}
show() {
this.modal.modal('show')
}
}
</script>
@endpush

View File

@ -0,0 +1,22 @@
@extends('layout.base')
@section('page_title')
@hasSection('brokers_title')
Operadores - @yield('brokers_title')
@else
Operadores
@endif
@endsection
@section('page_content')
<div class="ui container">
<h2 class="ui header">
@hasSection('brokers_header')
Operador - @yield('brokers_header')
@else
Operadores
@endif
</h2>
@yield('brokers_content')
</div>
@endsection

View File

@ -0,0 +1,489 @@
@extends('proyectos.brokers.base')
@section('brokers_title')
{{ $contract->broker->name }} - {{ $contract->project->descripcion }}
@endsection
@section('brokers_header')
{{ $contract->broker->name }} - {{ $contract->project->descripcion }}
@endsection
@include('layout.body.scripts.stats')
@prepend('page_scripts')
<script>
class TableHandler {
commission
constructor(commission) {
this.commission = commission
}
draw() {}
prices(units) {
return units.map(unit => {
let price = unit.valor ?? (unit.precio?.valor ?? 0)
const broker = price / (1 - this.commission)
const promotions = unit.promotions?.map(promotion => {
if (promotion.type === 1) {
return {
name: promotion.description,
type: promotion.type,
amount: promotion.amount,
final: broker + promotion.amount
}
}
return {
name: promotion.description,
type: promotion.type,
amount: promotion.amount,
final: broker / (1 - promotion.amount)
}
}) ?? []
return {
id: unit.id,
base: price,
commission: this.commission,
broker,
promotions
}
})
}
}
class GroupedTableHandler extends TableHandler {
promotions = {
names: new Set(),
values: []
}
process() {
return {
prices: prices => {
let promotions = {}
prices.map(price => price.promotions).forEach(promotionArray => {
promotionArray.forEach(p => {
if (!Object.hasOwn(promotions, p.name)) {
promotions[p.name] = {
name: p.name,
type: p.type,
amount: [],
final: []
}
}
promotions[p.name].amount.push(p.amount)
promotions[p.name].final.push(p.final)
})
})
return promotions
},
promotions: () => {
return {
names: promotions => {
Object.keys(promotions).forEach(name => {
this.add().promotion().name(name)
})
},
values: ({promotions, formatters}) => {
const temp = Object.values(promotions)
if (temp.length > 0) {
const data = temp.map(p => {
return {
name: p.name,
type: p.type,
amount: {
min: Math.min(...p.amount),
max: Math.max(...p.amount),
desv: Stat.standardDeviation(p.amount),
mean: Stat.mean(p.amount)
},
final: {
min: Math.min(...p.final),
max: Math.max(...p.final),
desv: Stat.standardDeviation(p.final),
mean: Stat.mean(p.final)
}
}
})
this.add().promotion().values({promotions: data, formatters})
return
}
this.promotions.values.push([])
}
}
}
}
}
add() {
return {
promotion: () => {
return {
name: name => {
this.promotions.names.add(name)
},
values: ({promotions, formatters}) => {
this.promotions.values.push(promotions.map(promotion => {
const amount_tooltip = [
`Min: ${promotion.type === 1 ? 'UF ' + formatters.ufs.format(promotion.amount.min) : formatters.percent.format(promotion.amount.min)}`,
`Max: ${promotion.type === 1 ? 'UF ' + formatters.ufs.format(promotion.amount.max) : formatters.percent.format(promotion.amount.max)}`,
`Desv: ${promotion.type === 1 ? 'UF ' + formatters.ufs.format(promotion.amount.desv) : formatters.percent.format(promotion.amount.desv)}`
].join("\n").replaceAll(' ', '&nbsp;')
const final_tooltip = [
`Min: UF ${formatters.ufs.format(promotion.final.min)}`,
`Max: UF ${formatters.ufs.format(promotion.final.max)}`,
`Desv: UF ${formatters.ufs.format(promotion.final.desv)}`
].join("\n").replaceAll(' ', '&nbsp;')
return {
name: promotion.name,
value: [
`<td class="right aligned"><span data-tooltip="${amount_tooltip}" data-position="right center" data-variation="wide multiline">${promotion.type === 1 ? formatters.ufs.format(promotion.amount.mean) : formatters.percent.format(promotion.amount.mean)}</td>`,
`<td class="right aligned"><span data-tooltip="${final_tooltip}" data-position="right center" data-variation="wide multiline">UF ${formatters.ufs.format(promotion.final.mean)}</td></td>`,
].join("\n")
}
}))
}
}
}
}
}
build() {
return {
promotions: (table, tbody) => {
if (this.promotions.names.size > 0) {
const title = document.getElementById(this.ids.promotions)
title.innerHTML = this.promotions.names.size > 0 ? Array.from(this.promotions.names)[0] : ''
title.setAttribute('colspan', '2')
if (this.promotions.names.size > 1) {
const thead = table.querySelector('thead')
Array.from(this.promotions.names).slice(1).forEach(name => {
thead.insertAdjacentHTML('beforeend', `<th class="right aligned" style="text-decoration: overline" colspan="2">${name}</th>`)
})
}
const trs = tbody.querySelectorAll('tr')
this.promotions.values.forEach((row, i) => {
const tr = trs[i]
const td = tr.querySelector('td.promotions')
if (row.length === 0) {
td.setAttribute('colspan', 2 * this.promotions.names.size)
return
}
td.remove()
this.promotions.names.forEach(name => {
const index = row.findIndex(r => r.name === name)
if (index === -1) {
tr.insertAdjacentHTML('beforeend', '<td colspan="2"></td>')
return
}
tr.insertAdjacentHTML('beforeend', row[index].value)
})
})
}
}
}
}
}
</script>
@endprepend
@section('brokers_content')
<div class="ui statistic">
<div class="value">{{ $format->percent($contract->commission ?? 0, 2, true) }}</div>
<div class="label">
Comisión desde {{ $contract->current()?->date?->format('d/m/Y') }}
</div>
</div>
<br />
<div class="ui card">
<div class="content">
<div class="header">
Promociones Aplicadas
</div>
<div class="description">
@if (count($contract->promotions()) === 0)
- Ninguna
@else
<div class="ui list">
@foreach ($contract->promotions() as $promotion)
<div class="item">
{{ $promotion->description }} {{ $format->percent($promotion->amount, 2, true) }} {{ ucwords($promotion->type->name()) }}
</div>
@endforeach
</div>
@endif
</div>
</div>
</div>
<div class="ui very basic segment">
<div class="ui active inline loader" id="loader"></div>
<div class="ui indicating progress" id="load_progress">
<div class="bar">
<div class="progress"></div>
</div>
</div>
</div>
<div id="results">
<div class="ui top attached tabular menu">
<a class="item active" data-tab="tipos">Tipos</a>
<a class="item" data-tab="lineas">Líneas</a>
<a class="item" data-tab="unidades">Unidades</a>
</div>
<div class="ui top attached indicating progress" id="values_progress">
<div class="bar"></div>
</div>
<div class="ui bottom attached tab basic fitted segment active" data-tab="tipos">
@include('proyectos.brokers.contracts.show.tipo')
</div>
<div class="ui bottom attached tab basic fitted segment" data-tab="lineas">
@include('proyectos.brokers.contracts.show.linea')
</div>
<div class="ui bottom attached tab basic fitted segment" data-tab="unidades">
@include('proyectos.brokers.contracts.show.unidades')
</div>
</div>
@endsection
@include('layout.body.scripts.datatables')
@include('layout.body.scripts.datatables.searchbuilder')
@include('layout.body.scripts.datatables.buttons')
@push('page_scripts')
<script>
const units = {
ids: {
units: '',
loader: '',
progress: '',
load_progress: ''
},
data: {
project_id: {{ $contract->project->id }},
commission: {{ $contract->commission }},
units: []
},
formatters: {
ufs: new Intl.NumberFormat('es-CL', { style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 }),
percent: new Intl.NumberFormat('es-CL', { style: 'percent', minimumFractionDigits: 2 })
},
handlers: {
tipo: null,
linea: null,
unit: null
},
get() {
return {
units: () => {
const url = `{{ $urls->api }}/proyecto/${units.data.project_id}/unidades`
return APIClient.fetch(url).then(response => response.json()).then(json => {
if (json.unidades.length === 0) {
console.error(json.errors)
return
}
units.data.units = []
Object.entries(json.unidades).forEach(([tipo, unidades]) => {
units.data.units = [...units.data.units, ...unidades]
})
})
},
promotions: progress_bar => {
const chunkSize = 100
const chunks = []
for (let i = 0; i < units.data.units.length; i += chunkSize) {
chunks.push(units.data.units.slice(i, i + chunkSize).map(u => u.id))
}
const promises = []
const url = `{{ $urls->api }}/proyectos/broker/{{ $contract->broker->rut }}/contract/{{ $contract->id }}/promotions`
const method = 'post'
chunks.forEach(chunk => {
const body = new FormData()
chunk.forEach(id => body.append('unidad_ids[]', id))
promises.push(APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
progress_bar.progress('increment', json.input.unidad_ids.length)
if (json.unidades.length === 0) {
return
}
json.unidades.forEach(unidad => {
const idx = units.data.units.findIndex(u => u.id === parseInt(unidad.id))
units.data.units[idx].promotions = unidad.promotions
})
}))
})
return Promise.all(promises)
},
prices: progress_bar => {
/*const unsold = [...units.data.units.filter(unit => !unit.sold), ...units.data.units.filter(unit => unit.sold && unit.proyecto_tipo_unidad.tipo_unidad.descripcion !== 'departamento')]
const current_total = progress_bar.progress('get total')
progress_bar.progress('set total', current_total + unsold.length)*/
const chunkSize = 100
const chunks = []
for (let i = 0; i < units.data.units.length; i += chunkSize) {
chunks.push(units.data.units.slice(i, i + chunkSize).map(u => u.id))
}
const promises = []
const url = `{{ $urls->api }}/proyecto/{{ $contract->project->id }}/unidades/precios`
const method = 'post'
chunks.forEach(chunk => {
const body = new FormData()
chunk.forEach(id => body.append('unidad_ids[]', id))
promises.push(APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
progress_bar.progress('increment', json.input.unidad_ids.length)
if (json.precios.length === 0) {
return
}
json.precios.forEach(unidad => {
const idx = units.data.units.findIndex(u => u.id === parseInt(unidad.id))
units.data.units[idx].precio = unidad.precio
})
}))
})
return Promise.all(promises)
},
values: progress_bar => {
const sold = units.data.units.filter(unit => unit.sold && unit.proyecto_tipo_unidad.tipo_unidad.descripcion === 'departamento')
progress_bar.progress('set total', sold.length)
const chunkSize = 10
const chunks = []
for (let i = 0; i < sold.length; i += chunkSize) {
chunks.push(sold.slice(i, i + chunkSize).map(u => u.id))
}
const promises = []
const url = `{{ $urls->api }}/ventas/by/unidades`
const method = 'post'
chunks.forEach(chunk => {
const body = new FormData()
chunk.forEach(id => body.append('unidad_ids[]', id))
promises.push(APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
progress_bar.progress('increment', json.input.unidad_ids.length)
if (json.ventas.length === 0) {
return
}
json.ventas.forEach(({unidad_id, venta}) => {
const unidades = venta.propiedad.unidades
const otras_unidades = unidades.filter(unit => unit.id !== parseInt(unidad_id) && unit.proyecto_tipo_unidad.tipo_unidad.descripcion !== 'departamento')
const departamentos = unidades.filter(unit => unit.proyecto_tipo_unidad.tipo_unidad.descripcion === 'departamento' && unit.id !== parseInt(unidad_id))
const precios = otras_unidades.map(unit => {
const idx = units.data.units.findIndex(u => u.id === unit.id)
return units.data.units[idx].precio?.valor ?? 0
}).reduce((sum, precio) => sum + precio, 0)
if (departamentos.length === 0) {
const idx = units.data.units.findIndex(unit => unit.id === parseInt(unidad_id))
units.data.units[idx].valor = venta.valor - precios
units.data.units[idx].venta = venta
return
}
const sum_precios = departamentos.map(departamento => {
const idx = units.data.units.findIndex(unit => unit.id === departamento.id)
return units.data.units[idx].precio
}).reduce((sum, precio) => sum + precio, 0)
departamentos.forEach(departamento => {
const idx = units.data.units.findIndex(unit => unit.id === departamento.id)
const saldo = venta.valor - precios
units.data.units[idx].valor = saldo / sum_precios * departamento.precio
units.data.units[idx].venta = venta
})
})
}))
})
return Promise.all(promises)
},
sold: progress_bar => {
const chunkSize = 100
const chunks = []
for (let i = 0; i < units.data.units.length; i += chunkSize) {
chunks.push(units.data.units.slice(i, i + chunkSize).map(u => u.id))
}
const promises = []
const url = `{{ $urls->api }}/proyecto/{{ $contract->project->id }}/unidades/estados`
const method = 'post'
chunks.forEach(chunk => {
const body = new FormData()
chunk.forEach(id => body.append('unidad_ids[]', id))
promises.push(APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
progress_bar.progress('increment', json.input.unidad_ids.length)
if (json.estados.length === 0) {
return
}
json.estados.forEach(unidad => {
const idx = units.data.units.findIndex(u => u.id === parseInt(unidad.id))
units.data.units[idx].sold = unidad.sold
})
}))
})
return Promise.all(promises)
},
}
},
draw() {
return {
units: () => {
units.handlers.units.draw({units: units.data.units, formatters: units.formatters})
},
tipos: () => {
units.handlers.tipo.draw({units: units.data.units, formatters: units.formatters})
},
lineas: () => {
units.handlers.lineas.draw({units: units.data.units, formatters: units.formatters})
}
}
},
setup(ids) {
units.ids = ids
units.handlers.tipo = new TipoTable(units.data.commission)
units.handlers.lineas = new LineasTable(units.data.commission)
units.handlers.units = new UnitsTable(units.data.commission)
$(`#${units.ids.results}`).find('.tabular.menu .item').tab({
onVisible: function(tabPath) {
if (tabPath !== 'unidades') {
return
}
$(this.querySelector('table')).DataTable().columns.adjust().draw()
this.querySelector('table').style.width = ''
}
})
document.getElementById(units.ids.results).style.visibility = 'hidden'
document.getElementById(units.ids.progress).style.visibility = 'hidden'
document.getElementById(units.ids.load_progress).style.visibility = 'hidden'
const loader = $(`#${units.ids.loader}`)
units.get().units().then(() => {
document.getElementById(units.ids.load_progress).style.visibility = 'visible'
const units_length = units.data.units.length
const progress_bar = $(`#${units.ids.load_progress}`)
progress_bar.progress({ total: units_length * 3 })
loader.hide()
units.get().promotions(progress_bar).then(() => {
units.get().sold(progress_bar).then(() => {
units.get().prices(progress_bar).then(() => {
document.getElementById(units.ids.results).style.visibility = 'visible'
loader.parent().remove()
units.draw().units()
units.draw().tipos()
units.draw().lineas()
document.getElementById(units.ids.progress).style.visibility = 'visible'
const progress_bar = $(`#${units.ids.progress}`)
progress_bar.progress()
units.get().values(progress_bar).then(() => {
document.getElementById(units.ids.progress).remove()
units.draw().units()
units.draw().tipos()
units.draw().lineas()
})
})
})
})
})
}
}
$(document).ready(function () {
units.setup({results: 'results', loader: 'loader', progress: 'values_progress', load_progress: 'load_progress'})
})
</script>
@endpush

View File

@ -0,0 +1,91 @@
<table class="ui table" id="lineas">
<thead>
<tr>
<th>Tipo</th>
<th class="center aligned">Línea</th>
<th class="center aligned">Orientación</th>
<th class="center aligned">Cantidad</th>
<th class="right aligned" style="text-decoration: overline">Precio Base</th>
<th class="right aligned" style="text-decoration: overline">Comisión</th>
<th class="right aligned" style="text-decoration: overline">Precio Operador</th>
<th class="center aligned" style="text-decoration: overline" id="linea_promociones">Promociones</th>
</tr>
</thead>
<tbody></tbody>
</table>
@push('page_scripts')
<script>
class LineasTable extends GroupedTableHandler {
ids = {
lineas: 'lineas',
promotions: 'linea_promociones'
}
constructor(commission) {
super(commission)
}
draw({units, formatters}) {
const table = document.getElementById(this.ids.lineas)
const tbody = table.querySelector('tbody')
tbody.innerHTML = ''
const lineas = Object.groupBy(units, unit => {
return [unit.proyecto_tipo_unidad.tipo_unidad.descripcion, unit.proyecto_tipo_unidad.nombre, unit.orientacion]
})
this.promotions.names = new Set()
this.promotions.values = []
Object.entries(lineas).sort(([linea1, unidades1], [linea2, unidades2]) => {
const split1 = linea1.split(',')
const split2 = linea2.split(',')
const ct = unidades1[0].proyecto_tipo_unidad.tipo_unidad.orden - unidades2[0].proyecto_tipo_unidad.tipo_unidad.orden
if (ct === 0) {
const cl = split1[1].localeCompare(split2[1])
if (cl === 0) {
return split1[2].localeCompare(split2[2])
}
return cl
}
return ct
}).forEach(([linea, unidades]) => {
const parts = linea.split(',')
const tipo = parts[0]
const orientacion = parts[2]
const prices = this.prices(unidades)
const base_tooltip = [
`Min: UF ${formatters.ufs.format(Math.min(...prices.map(p => p.base)))}`,
`Max: UF ${formatters.ufs.format(Math.max(...prices.map(p => p.base)))}`,
`Desv: UF ${formatters.ufs.format(Stat.standardDeviation(prices.map(p => p.base)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const commission_tooltip = [
`Min: ${formatters.percent.format(Math.min(...prices.map(p => p.commission)))}`,
`Max: ${formatters.percent.format(Math.max(...prices.map(p => p.commission)))}`,
`Desv: ${formatters.percent.format(Stat.standardDeviation(prices.map(p => p.commission)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const broker_tooltip = [
`Min: ${formatters.ufs.format(Math.min(...prices.map(p => p.broker)))}`,
`Max: ${formatters.ufs.format(Math.max(...prices.map(p => p.broker)))}`,
`Desv: ${formatters.ufs.format(Stat.standardDeviation(prices.map(p => p.broker)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const promotions = this.process().prices(prices)
this.process().promotions().names(promotions)
this.process().promotions().values({promotions, formatters})
tbody.innerHTML += [
`<tr>`,
`<td>${tipo.charAt(0).toUpperCase() + tipo.slice(1)}</td>`,
`<td class="center aligned"><span data-tooltip="${unidades[0].proyecto_tipo_unidad.tipologia}" data-position="right center">${parts[1]}</span></td>`,
`<td class="center aligned">${orientacion}</td>`,
`<td class="center aligned">${unidades.length}</td>`,
`<td class="right aligned"><span data-tooltip="${base_tooltip}" data-position="right center" data-variation="wide multiline">UF ${formatters.ufs.format(Stat.mean(prices.map(p => p.base)))}</span></td>`,
`<td class="right aligned"><span data-tooltip="${commission_tooltip}" data-position="right center" data-variation="wide multiline">${formatters.percent.format(Stat.mean(prices.map(p => p.commission)))}</span></td>`,
`<td class="right aligned"><span data-tooltip="${broker_tooltip}" data-position="right center" data-variation="wide multiline">UF ${formatters.ufs.format(Stat.mean(prices.map(p => p.broker)))}</span></td>`,
`<td class="right aligned promotions"></td>`,
`</tr>`
].join("\n")
})
this.build().promotions(table, tbody)
}
}
</script>
@endpush

View File

@ -0,0 +1,71 @@
<table class="ui table" id="tipos">
<thead>
<tr>
<th>Tipo</th>
<th class="center aligned">Cantidad</th>
<th class="right aligned" style="text-decoration: overline">Precio Base</th>
<th class="right aligned" style="text-decoration: overline">Comisión</th>
<th class="right aligned" style="text-decoration: overline">Precio Operador</th>
<th class="center aligned" style="text-decoration: overline" id="tipo_promociones">Promociones</th>
</tr>
</thead>
<tbody></tbody>
</table>
@push('page_scripts')
<script>
class TipoTable extends GroupedTableHandler {
ids = {
tipos: 'tipos',
promotions: 'tipo_promociones'
}
constructor(commission) {
super(commission)
}
draw({units, formatters}) {
const table = document.getElementById(this.ids.tipos)
const tbody = table.querySelector('tbody')
tbody.innerHTML = ''
const groups = Object.groupBy(units, unit => {
return unit.proyecto_tipo_unidad.tipo_unidad.descripcion
})
this.promotions.names = new Set()
this.promotions.values = []
Object.entries(groups).forEach(([tipo, unidades]) => {
const prices = this.prices(unidades)
const base_tooltip = [
`Min: UF ${formatters.ufs.format(Math.min(...prices.map(p => p.base)))}`,
`Max: UF ${formatters.ufs.format(Math.max(...prices.map(p => p.base)))}`,
`Desv: UF ${formatters.ufs.format(Stat.standardDeviation(prices.map(p => p.base)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const commission_tooltip = [
`Min: ${formatters.percent.format(Math.min(...prices.map(p => p.commission)))}`,
`Max: ${formatters.percent.format(Math.max(...prices.map(p => p.commission)))}`,
`Desv: ${formatters.percent.format(Stat.standardDeviation(prices.map(p => p.commission)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const broker_tooltip = [
`Min: ${formatters.ufs.format(Math.min(...prices.map(p => p.broker)))}`,
`Max: ${formatters.ufs.format(Math.max(...prices.map(p => p.broker)))}`,
`Desv: ${formatters.ufs.format(Stat.standardDeviation(prices.map(p => p.broker)))}`
].join("\n").replaceAll(' ', '&nbsp;')
const promotions = this.process().prices(prices)
this.process().promotions().names(promotions)
this.process().promotions().values({promotions, formatters})
tbody.innerHTML += [
`<tr>`,
`<td>${tipo.charAt(0).toUpperCase() + tipo.slice(1)}</td>`,
`<td class="center aligned">${unidades.length}</td>`,
`<td class="right aligned"><span data-tooltip="${base_tooltip}" data-position="right center" data-variation="wide multiline">UF ${formatters.ufs.format(Stat.mean(prices.map(p => p.base)))}</span></td>`,
`<td class="right aligned"><span data-tooltip="${commission_tooltip}" data-position="right center" data-variation="wide multiline">${formatters.percent.format(Stat.mean(prices.map(p => p.commission)))}</span></td>`,
`<td class="right aligned"><span data-tooltip="${broker_tooltip}" data-position="right center" data-variation="wide multiline">UF ${formatters.ufs.format(Stat.mean(prices.map(p => p.broker)))}</span></td>`,
`<td class="right aligned promotions"></td>`,
`</tr>`
].join("\n")
})
this.build().promotions(table, tbody)
}
}
</script>
@endpush

View File

@ -0,0 +1,254 @@
<table class="ui table" id="unidades">
<thead>
<tr>
<th>Estado</th>
<th>Tipo</th>
<th>Tipo Order</th>
<th class="right aligned">Unidad</th>
<th>Unidad Orden</th>
<th>Tipología</th>
<th>Piso</th>
<th>Orientación</th>
<th>m&#0178; Interior</th>
<th>m&#0178; Terraza</th>
<th>m&#0178; Vendibles</th>
<th>m&#0178; Total</th>
<th class="right aligned">Precio Base</th>
<th class="right aligned">Comisión</th>
<th class="right aligned">Precio Operador</th>
<th class="right aligned">UF/</th>
<th class="center aligned" id="unit_promotions">Promociones</th>
</tr>
</thead>
<tbody></tbody>
</table>
@push('page_scripts')
<script>
class UnitsTable extends TableHandler {
ids = {
units: 'unidades',
}
columns = [
'estado',
'tipo',
'tipo_order',
'unidad',
'unidad_order',
'tipologia',
'piso',
'orientacion',
'metros_interior',
'metros_terraza',
'metros',
'metros_total',
'precio_base',
'commission',
'precio_operador',
'UF/m²',
'promociones',
]
set = {
table: false,
titles: false
}
table
constructor(commission) {
super(commission)
}
setup() {
return {
titles: promotions_names => {
if (this.set.titles || promotions_names.size === 0) {
return
}
const nameArray = Array.from(promotions_names)
this.columns.pop()
this.columns.push(`${nameArray[0].toLowerCase().replaceAll(' ', '_')}.amount`)
this.columns.push(`${nameArray[0].toLowerCase().replaceAll(' ', '_')}.final`)
const table = document.getElementById(this.ids.units)
const thead = table.querySelector('thead')
const tr = thead.querySelector('tr')
const th = tr.querySelector('th#unit_promotions')
th.innerHTML = `${nameArray[0]}<br />Porcentaje`
tr.insertAdjacentHTML('beforeend', `<th class="right aligned">${nameArray[0]}<br />Precio Final</th>`)
if (promotions_names.size > 1) {
nameArray.slice(1).forEach(name => {
this.columns.push(`${name.toLowerCase().replaceAll(' ', '_')}.amount`)
this.columns.push(`${name.toLowerCase().replaceAll(' ', '_')}.final`)
tr.insertAdjacentHTML('beforeend', `<th class="right aligned">${name}<br />Porcentaje</th>`)
tr.insertAdjacentHTML('beforeend', `<th class="right aligned">${name}<br />Precio Final</th>`)
})
}
this.set.titles = true
},
table: () => {
if (typeof this.table !== 'undefined' || this.set.table) {
return
}
const dto = structuredClone(datatables_defaults)
dto.pageLength = 100
dto.columnDefs = [
{
target: ['tipo_order', 'unidad_order', 'tipologia', 'piso', 'orientacion', 'metros'].map(column => this.columns.indexOf(column)),
visible: false
},
{
target: ['tipo'].map(column => this.columns.indexOf(column)),
orderData: ['tipo_order'].map(column => this.columns.indexOf(column)),
},
{
target: ['unidad'].map(column => this.columns.indexOf(column)),
orderData: ['unidad_order'].map(column => this.columns.indexOf(column)),
},
{
target: ['unidad', 'precio_base', 'commission', 'precio_operador', 'UF/m²', 'porcentaje', 'precio_final']
.map(column => this.columns.indexOf(column)),
className: 'dt-right right aligned'
}
]
dto.order = ['tipo_order', 'unidad_order'].map(column => [this.columns.indexOf(column), 'asc'])
dto.language.searchBuilder = searchBuilder
const exportColumns = ['tipo', 'unidad', 'tipologia', 'piso', 'orientacion', 'metros_interior', 'metros_terraza', 'metros', 'metros_total', 'precio_operador', 'promociones']
if (typeof this.columns['promotions'] === 'undefined') {
exportColumns.pop()
this.columns.slice(this.columns.indexOf('UF/m²')+1).forEach(name => {
exportColumns.push(name)
})
}
dto.layout = {
top1Start: {
searchBuilder: {
columns: this.columns.filter(column => !['tipo_order', 'unidad_order'].includes(column))
.map(column => this.columns.indexOf(column)),
}
},
top1End: {
buttons: [
{
extend: 'excelHtml5',
className: 'green',
text: 'Exportar a Excel <i class="file excel icon"></i>',
title: 'Lista de Precios - {{ $contract->broker->name }} - {{ $contract->project->descripcion }} - {{ (new DateTime())->format('Y-m-d') }}',
download: 'open',
exportOptions: {
columns: exportColumns
.map(column => this.columns.indexOf(column)),
rows: (idx, data, node) => {
return data[this.columns.indexOf('estado')] === 'Libre'
},
format: {
body: (data, row, columnIdx, node) => {
if (typeof data === 'string' && data.includes('<span')) {
return data.replace(/<span.*>(.*)<\/span>/, '$1')
}
if (typeof data === 'string' && data.includes('UF ')) {
return data.replace('UF ', '').replaceAll('.', '').replaceAll(',', '.')
}
const formatColumns = ['metros', 'metros_interior', 'metros_terraza', 'metros_total']
if (this.set.titles) {
this.columns.filter(column => column.includes('.amount') || column.includes('.final')).forEach(name => {
formatColumns.push(name)
})
}
if (formatColumns.map(column => this.columns.indexOf(column)).includes(columnIdx)) {
return data.replaceAll('.', '').replaceAll(',', '.')
}
return data
}
}
},
}
]
}
}
this.table = $(`#${this.ids.units}`).DataTable(dto)
this.set.table = true
}
}
}
draw({units, formatters}) {
const prices = this.prices(units)
const promotions_names = new Set()
const tableData = []
units.forEach(unidad => {
const tipo = unidad.proyecto_tipo_unidad.tipo_unidad.descripcion
const price = prices.find(p => p.id === unidad.id)
let promotions = []
price.promotions.forEach(p => {
if (Object.hasOwn(promotions, p.name)) {
return
}
promotions[p.name] = {
name: p.name,
type: p.type,
amount: p.type === 1 ? 'UF ' + formatters.ufs.format(p.amount) : formatters.percent.format(p.amount),
final: `UF ${formatters.ufs.format(p.final)}`
}
})
Object.keys(promotions).forEach(name => {
promotions_names.add(name)
})
const temp = Object.values(promotions)
const data = [
unidad.sold ? `<span class="ui yellow text">Vendida</span>` : 'Libre',
tipo.charAt(0).toUpperCase() + tipo.slice(1),
unidad.proyecto_tipo_unidad.tipo_unidad.orden,
unidad.sold && unidad.venta ? `<a href="{{ $urls->base }}/venta/${unidad.venta?.id }" data-tooltip="Valor Promesa: UF ${formatters.ufs.format(unidad.venta?.valor ?? 0)}" data-position="left center">${unidad.descripcion}</a>` : unidad.descripcion,
unidad.descripcion.padStart(4, '0'),
unidad.proyecto_tipo_unidad.tipologia,
unidad.piso,
unidad.orientacion,
formatters.ufs.format(unidad.proyecto_tipo_unidad.util) ?? 0,
formatters.ufs.format(unidad.proyecto_tipo_unidad.terraza) ?? 0,
formatters.ufs.format(unidad.proyecto_tipo_unidad.vendible) ?? 0,
formatters.ufs.format(unidad.proyecto_tipo_unidad.superficie) ?? 0,
'UF ' + formatters.ufs.format(price.base ?? 0),
formatters.percent.format(price.commission ?? 0),
'UF ' + formatters.ufs.format(price.broker ?? 0),
formatters.ufs.format(price.broker / unidad.proyecto_tipo_unidad.vendible),
''
]
if (temp.length > 0) {
data.pop()
temp.forEach((p, i) => {
data.push(p.amount)
data.push(p.final)
})
} else {
if (promotions_names.size > 0) {
Array.from(promotions_names).forEach(name => {
data.push('')
data.push('')
})
}
}
tableData.push(data)
})
this.setup().titles(promotions_names)
this.setup().table()
const table = this.table
table.clear()
table.rows.add(tableData)
table.draw()
}
}
</script>
@endpush

View File

@ -0,0 +1,113 @@
<div class="ui modal" id="edit_broker_modal">
<div class="header">
Editar Operador
</div>
<div class="content">
<form class="ui form" id="edit_broker_form">
<input type="hidden" name="broker_rut" value="" />
<div class="fields">
<div class="field">
<label>Rut</label>
<div class="ui right labeled input">
<input type="text" name="rut" placeholder="Rut" value="" disabled />
<div class="ui basic label">-<span id="edit_digit"></span></div>
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Nombre</label>
<input type="text" name="name" placeholder="Nombre" value="" />
</div>
<div class="six wide field">
<label>Razón Social</label>
<input type="text" name="legal_name" placeholder="Razón Social" value="" />
</div>
</div>
<div class="fields">
<div class="field">
<label>Contacto</label>
<input type="text" name="contact" placeholder="Contacto" value="" />
</div>
</div>
<div class="fields">
<div class="field">
<label>Correo</label>
<input type="email" name="email" placeholder="Correo" value="" />
</div>
<div class="field">
<label>Teléfono</label>
<input type="text" name="phone" placeholder="Teléfono" value="" />
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Guardar
<i class="checkmark icon"></i>
</div>
</div>
</div>
@push('page_scripts')
<script>
class EditModal
{
ids
modal
handler
data
constructor(handler)
{
this.handler = handler
this.ids = {
modal: 'edit_broker_modal',
form: 'edit_broker_form',
digit: 'edit_digit',
}
this.modal = $(`#${this.ids.modal}`)
this.modal.modal({
onApprove: () => {
const form = document.getElementById(this.ids.form)
const broker_rut = form.querySelector('[name="broker_rut"]').value
const data = {
rut: form.querySelector('[name="rut"]').value.replace(/\D/g, ''),
name: form.querySelector('[name="name"]').value,
contact: form.querySelector('[name="contact"]').value || '',
legal_name: form.querySelector('[name="legal_name"]').value,
email: form.querySelector('[name="email"]').value || '',
phone: form.querySelector('[name="phone"]').value || ''
}
this.handler.execute().edit(data)
}
})
this.modal.modal('hide')
}
load(data) {
this.data = data
const form = document.getElementById(this.ids.form)
form.querySelector('input[name="broker_rut"]').value = data.rut
form.querySelector('input[name="rut"]').value = data.rut
form.querySelector('input[name="name"]').value = data.name
form.querySelector('input[name="legal_name"]').value = data.legal_name
form.querySelector('input[name="contact"]').value = data.contact
form.querySelector('input[name="email"]').value = data.email
form.querySelector('input[name="phone"]').value = data.phone
this.update().digit(data.rut)
this.modal.modal('show')
}
update() {
return {
digit: value => {
document.getElementById(this.ids.digit).textContent = Rut.digitoVerificador(value)
},
}
}
}
</script>
@endpush

View File

@ -0,0 +1,52 @@
<div class="ui top attached right aligned basic segment">
<button type="button" class="ui mini tertiary icon button" id="refresh_button">
<i class="sync alternate icon"></i>
</button>
<button type="button" class="ui mini tertiary icon button" id="up_button">
<i class="arrow up icon"></i>
</button>
</div>
<table class="ui table" id="projects">
<thead>
<tr>
<th>Proyecto</th>
</tr>
</thead>
<tbody>
@foreach($projects as $project)
<tr data-index="{{$project->id}}">
<td class="link" colspan="2">{{$project->descripcion}}</td>
</tr>
@endforeach
</tbody>
</table>
@push('page_scripts')
<script>
$(document).ready(function () {
document.querySelectorAll('#projects td.link').forEach(column => {
column.style.cursor = 'pointer'
column.addEventListener('click', () => {
const index = column.parentNode.dataset.index
if (typeof brokers.data.contracts[index] !== 'undefined') {
brokers.data.project_id = index
brokers.draw().brokers(index)
return
}
brokers.get().contracts(index)
})
})
document.getElementById('refresh_button').addEventListener('click', () => {
if (brokers.data.project_id === null) {
return
}
brokers.actions().refresh()
})
document.getElementById('up_button').addEventListener('click', () => {
brokers.actions().up()
})
})
</script>
@endpush

View File

@ -0,0 +1,170 @@
@extends('proyectos.brokers.base')
@section('brokers_title')
{{ $broker->name }}
@endsection
@section('brokers_header')
{{ $broker->name }}
@endsection
@section('brokers_content')
<div class="ui compact segment">
<b>RUT:</b> {{ $broker->rutFull() }} <br />
<b>Razón Social:</b> {{ $broker->data()?->legalName }}
</div>
@if ($broker->data()?->representative->name !== null)
<div class="ui card">
<div class="content">
<div class="header">
Contacto
</div>
<div class="description">
Nombre: {{ $broker->data()?->representative?->name }}<br />
Email: {{ $broker->data()?->representative?->email }}<br />
Teléfono: {{ $broker->data()?->representative?->phone }}<br />
Dirección: {{ $broker->data()?->representative?->address }}
</div>
</div>
</div>
@endif
<table class="ui table">
<thead>
<tr>
<th>Proyecto</th>
<th>Comisión</th>
<th>Fecha Inicio</th>
<th class="right aligned">
<button type="button" class="ui small tertiary green icon button" id="add_contract_button">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
<tbody id="contracts">
@foreach($broker->contracts() as $contract)
<tr>
<td>
<a href="{{ $urls->base }}/proyectos/broker/{{ $broker->rut }}/contract/{{ $contract->id }}">
{{ $contract->project->descripcion }}
<i class="angle right icon"></i>
</a>
</td>
<td>{{ $format->percent($contract->commission, 2, true) }}</td>
<td>{{ $contract->current()->date->format('d-m-Y') }}</td>
<td class="right aligned">
<button type="button" class="ui small tertiary red icon button remove_button" data-index="{{$contract->id}}">
<i class="remove icon"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@include('proyectos.brokers.show.add_modal')
@endsection
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script>
const brokerHandler = {
ids: {
buttons: {
add: '',
remove: ''
}
},
modals: {
add: null,
},
execute() {
return {
add: data => {
const url = '{{ $urls->api }}/proyectos/broker/{{ $broker->rut }}/contracts/add'
const method = 'post'
const body = new FormData()
body.set('contracts[]', JSON.stringify(data))
return APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
console.error(response.errors)
alert('No se pudo agregar contrato.')
return
}
return response.json().then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo agregar contrato.')
return
}
window.location.reload()
})
})
},
remove: index => {
const url = `{{ $urls->api }}/proyectos/brokers/contract/${index}`
const method = 'delete'
return APIClient.fetch(url, {method}).then(response => {
if (!response) {
console.error(response.errors)
alert('No se pudo eliminar contrato.')
return
}
return response.json().then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo eliminar contrato.')
return
}
window.location.reload()
})
})
}
}
},
events() {
return {
add: clickEvent => {
clickEvent.preventDefault()
brokerHandler.modals.add.show()
},
remove: clickEvent => {
clickEvent.preventDefault()
const index = $(clickEvent.currentTarget).data('index')
brokerHandler.execute().remove(index)
}
}
},
buttonWatch() {
document.getElementById(brokerHandler.ids.buttons.add).addEventListener('click', brokerHandler.events().add)
Array.from(document.getElementsByClassName(brokerHandler.ids.buttons.remove)).forEach(button => {
button.addEventListener('click', brokerHandler.events().remove)
})
},
setup(ids) {
brokerHandler.ids = ids
brokerHandler.buttonWatch()
this.modals.add = new AddModal(brokerHandler)
const dto = structuredClone(datatables_defaults)
dto.order = [[0, 'asc']]
dto.columnDefs = [
{
targets: 3,
orderable: false
}
]
$('#contracts').parent().DataTable(dto)
}
}
$(document).ready(() => {
brokerHandler.setup({
buttons: {
add: 'add_contract_button',
remove: 'remove_button'
}
})
})
</script>
@endpush

View File

@ -0,0 +1,96 @@
<div class="ui modal" id="add_contract_modal">
<div class="header">
Agregar Contrato
</div>
<div class="content">
<form class="ui form" id="add_contract_form">
<input type="hidden" name="broker_rut" value="{{$broker->rut}}" />
<div class="fields">
<div class="six wide field">
<label>Proyecto</label>
<div class="ui search selection dropdown" id="project">
<input type="hidden" name="project_id" />
<i class="dropdown icon"></i>
<div class="default text">Proyecto</div>
<div class="menu">
@foreach($projects as $project)
<div class="item" data-value="{{ $project->id }}">{{ $project->descripcion }}</div>
@endforeach
</div>
</div>
</div>
<div class="field">
<label>Comisión</label>
<div class="ui right labeled input">
<input type="text" name="commission" placeholder="Comisión" />
<div class="ui basic label">%</div>
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Fecha Inicio</label>
<div class="ui calendar" id="add_fecha_inicio">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="date" />
</div>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Agregar
<i class="checkmark icon"></i>
</div>
</div>
</div>
@push('page_scripts')
<script>
class AddModal {
ids
modal
handler
constructor(handler) {
this.handler = handler
this.ids = {
modal: 'add_contract_modal',
form: 'add_contract_form',
proyecto: 'project',
date: 'add_fecha_inicio'
}
$(`#${this.ids.proyecto}`).dropdown()
const cdo = structuredClone(calendar_date_options)
cdo['initialDate'] = new Date()
$(`#${this.ids.date}`).calendar(cdo)
this.modal = $(`#${this.ids.modal}`)
this.modal.modal({
onApprove: () => {
const form = document.getElementById(this.ids.form)
let commission = parseFloat(form.querySelector('[name="commission"]').value)
if (commission > 1) {
commission /= 100
}
const date = $(`#${this.ids.date}`).calendar('get date')
const data = {
broker_rut: form.querySelector('[name="broker_rut"]').value,
project_id: form.querySelector('[name="project_id"]').value,
commission: commission,
date: [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-')
}
this.handler.execute().add(data)
}
})
}
show() {
this.modal.modal('show')
}
}
</script>
@endpush

View File

@ -35,7 +35,7 @@
@endsection
@push('page_scripts')
<script type="text/javascript">
<script>
class Proyecto {
id
estados

View File

@ -15,7 +15,7 @@
<table class="ui striped table">
<tr>
<td>Dirección</td>
<td>{{$proyecto->direccion()}}</td>
<td>{{$proyecto->direccion()}} <a href="#" id="edit_direccion_button"><i class="small edit icon"></i></a></td>
</tr>
<tr>
<td>Inmobiliaria</td>
@ -137,10 +137,11 @@
</div>
@endsection
@include('proyectos.show.edit_direccion')
@include('layout.body.scripts.chartjs')
@push('page_scripts')
<script type="text/javascript">
<script>
const superficies = {
ids: {
vendible: '',
@ -485,6 +486,11 @@
ventas.setup({id_ventas: '#ventas', id_stock: '#stock', id_proyeccion: '#proyeccion',
id_chart_general: '#chart_venta_general', id_chart_tipologias: '#chart_venta_tipologia',
id_chart_velocidad: '#chart_venta_velocidad'})
document.getElementById('edit_direccion_button').addEventListener('click', event => {
event.preventDefault()
$('#edit_direccion_modal').modal('show')
})
})
</script>
@endpush

View File

@ -0,0 +1,136 @@
<div class="ui modal" id="edit_direccion_modal">
<div class="header">
Editar Dirección
</div>
<div class="content">
<form class="ui form" id="edit_direccion_form">
<input type="hidden" name="id" />
<div class="fields">
<div class="field">
<label>Calle</label>
<input type="text" name="calle" value="{{ $proyecto->direccion()->calle }}" />
</div>
<div class="field">
<label>Número</label>
<input type="text" name="numero" value="{{ $proyecto->direccion()->numero }}" />
</div>
<div class="field">
<label>Extra</label>
<input type="text" name="extra" value="{{ $proyecto->direccion()->extra }}" />
</div>
</div>
<div class="fields">
<div class="field">
<label>Comuna</label>
<div class="ui search selection dropdown" id="comuna">
<input type="hidden" name="comuna" />
<i class="dropdown icon"></i>
<div class="default text">Comuna</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>Región</label>
<div class="ui search selection dropdown" id="region">
<input type="hidden" name="region" />
<i class="dropdown icon"></i>
<div class="default text">Región</div>
<div class="menu"></div>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui green ok button">
Guardar
</div>
</div>
</div>
@push('page_scripts')
<script>
function submitForm() {
const form = document.getElementById('edit_direccion_form')
const body = new FormData(form)
const url = '{{$urls->api}}/direccion/{{ $proyecto->direccion()->id }}/edit'
const method = 'post'
APIClient.fetch(url, {method, body}).then(response => {
if (response.ok) {
return response.json().then(json => {
if (json.success) {
window.location.reload()
}
})
}
})
}
function fetchComunas(region_id) {
const url = `{{ $urls->api }}/region/${region_id}/comunas`
APIClient.fetch(url).then(response => {
if (!response.ok) {
return;
}
return response.json().then(json => {
const dropdown = $('#comuna')
const values = []
json.comunas.forEach(comuna => {
values.push({
name: comuna.descripcion,
value: comuna.id,
text: comuna.descripcion
})
})
dropdown.dropdown('change values', values)
if (json.region_id === {{ $proyecto->direccion()->comuna->provincia->region->id }}) {
dropdown.dropdown('set selected', {{ $proyecto->direccion()->comuna->id }})
}
})
})
}
function fetchRegiones() {
const url = '{{ $urls->api }}/regiones'
APIClient.fetch(url).then(response => {
if (!response.ok) {
return;
}
return response.json().then(json => {
const dropdown = $('#region')
const values = []
json.regiones.forEach(region => {
values.push({
name: region.descripcion,
value: region.id,
text: region.descripcion
})
})
dropdown.dropdown({
values,
onChange: (value, text, $choice) => {
fetchComunas(value)
}
})
$('#comuna').dropdown()
dropdown.dropdown('set selected', {{ $proyecto->direccion()->comuna->provincia->region->id }})
})
})
}
$(document).ready(function() {
fetchRegiones()
$('#edit_direccion_modal').modal({
onApprove: function() {
submitForm()
}
})
document.getElementById('edit_direccion_form').addEventListener('submit', event => {
event.preventDefault()
submitForm()
return false
})
})
</script>
@endpush

View File

@ -39,7 +39,7 @@
@endpush
@push('page_scripts')
<script type="text/javascript">
<script>
class Unidad
{
id

View File

@ -31,7 +31,7 @@
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script type="text/javascript">
<script>
class Row
{
proyecto
@ -45,6 +45,9 @@
draw() {
const tipo = this.unidad.proyecto_tipo_unidad.tipo_unidad.descripcion
let unidad = tipo.charAt(0).toUpperCase() + tipo.slice(1) + ' ' + this.unidad.descripcion
if (typeof this.venta !== 'undefined') {
unidad = this.venta.propiedad.tipologia
}
let precio = 0
let propietario = ''
let fecha = ''
@ -55,6 +58,7 @@
if (!this.venta.current_estado.tipo_estado_venta.activa) {
unidad.html(unidad.html() + ' (I)')
}
unidad.html(unidad.html() + ' <i class="angle right icon"></i>')
propietario = $('<a></a>')
.attr('href','{{$urls->base}}/search/"' + encodeURIComponent(this.venta.propietario.nombre_completo) + '"/propietario')
.html(this.venta.propietario.nombre_completo)

View File

@ -8,7 +8,7 @@
<div class="inline field">
<div class="ui calendar" id="fecha_venta_calendar">
<div class="ui icon input">
<input type="text" name="fecha_venta" id="fecha_venta" />
<input type="text" name="fecha_venta" id="fecha_venta" required />
<i class="calendar icon"></i>
</div>
</div>
@ -34,6 +34,7 @@
<div class="four wide field">
<div class="ui fluid search selection dropdown" id="proyecto">
<input type="hidden" name="proyecto" />
<i class="dropdown icon"></i>
<div class="default text">Proyecto</div>
<div class="menu">
@foreach($proyectos as $proyecto)
@ -42,27 +43,32 @@
</div>
</div>
</div>
<button class="ui button" type="button" id="agregar_departamento">
<button class="ui button" type="button" id="agregar_departamento" title="Agregar Departamento">
<i class="plus icon"></i>
<i class="building icon"></i>
{{--Departamento--}}
</button>
<button class="ui button" type="button" id="agregar_estacionamiento">
<button class="ui button" type="button" id="agregar_estacionamiento" title="Agregar Estacionamiento">
<i class="plus icon"></i>
<i class="car icon"></i>
{{--Estacionamiento--}}
</button>
<button class="ui button" type="button" id="agregar_bodega">
<button class="ui button" type="button" id="agregar_bodega" title="Agregar Bodega">
<i class="plus icon"></i>
<i class="warehouse icon"></i>
{{--Bodega--}}
</button>
<button class="ui button" type="button" id="agregar_terraza" title="Agregar Terraza">
<i class="plus icon"></i>
<i class="vector square icon"></i>
</button>
<div id="unidades" class="ui ordered list"></div>
<h4 class="ui dividing header">FORMA DE PAGO</h4>
<label for="valor">Valor</label>
<div class="inline field">
<div class="ui right labeled input">
<input type="text" name="valor" id="valor" />
<input type="text" name="valor" id="valor" required />
<div class="ui label">UF</div>
</div>
</div>
@ -144,15 +150,17 @@
</div>
@endsection
@include('layout.body.scripts.rut')
@push('page_scripts')
<script type="text/javascript">
<script>
const regiones = [
@foreach ($regiones as $region)
'<div class="item" data-value="{{$region->id}}">{{$region->descripcion}}</div>',
'<div class="item" data-value="{{$region->id}}">{{$region->numeral}} - {{$region->descripcion}}</div>',
@endforeach
]
class Rut {
class RutHandler {
ids
patterns
valid
@ -181,7 +189,7 @@
}
clean() {
return {
rut: rut => rut.includes('-') ? rut.split('-').join('') : rut
rut: rut => rut.includes('-') || rut.includes('.') ? rut.replaceAll('-', '').replaceAll('.', '') : rut
}
}
get() {
@ -195,26 +203,20 @@
verifier: digits => {
let sum = 0
let mul = 2
let i = digits.length
while (i--) {
sum = sum + parseInt(digits.charAt(i)) * mul
if (mul % 7 === 0) {
mul = 2
} else {
mul++
}
sum = sum + parseInt(digits.charAt(i)) * mul;
(mul % 7 === 0) ? mul = 2 : mul++
}
const res = sum % 11
if (res === 0) {
return '0'
} else if (res === 1) {
return 'k'
switch (res) {
case 0:
case 1:
return res.toString()
default:
return `${11 - res}`
}
return `${11 - res}`
}
}
}
@ -222,9 +224,10 @@
if (!this.is().like(rut) || (not_suspicious && this.is().suspicious(rut))) {
return false
}
return this.get().verifier(rut).toLowerCase() === this.calculate().verifier(this.get().digits(rut))
return Rut.validar(this.get().digits(rut), this.get().verifier(rut))
}
verify(event) {
this.alert().valid()
const input = $(event.currentTarget)
const val = input.val()
let new_val = this.clean().rut(val)
@ -237,7 +240,6 @@
this.alert().invalid()
return
}
this.alert().valid()
this.valid(new_val)
}
alert() {
@ -262,7 +264,9 @@
provincias: []
}
$(this.id).dropdown()
$(this.id).dropdown({
forceSelection: true
})
}
get() {
@ -332,6 +336,7 @@
const dd = $(this.id)
dd.dropdown({
fireOnInit: true,
forceSelection: true,
onChange: value => {
this.comuna.data.region = value
this.comuna.get().provincias()
@ -349,60 +354,71 @@
const lines = [
'<label for="rut">RUT</label>',
'<div class="inline field">',
'<input type="text" id="rut" name="rut" placeholder="00000000-0" />',
'<span class="ui error message" id="alert_rut">',
'<i class="exclamation triangle icon"></i>',
'RUT Inválido',
'</span>',
'<input type="text" id="rut" name="rut" placeholder="00000000-0" required />',
'<span class="ui error message" id="alert_rut">',
'<i class="exclamation triangle icon"></i>',
'RUT Inválido',
'</span>',
'</div>',
'<label for="nombres">Nombre</label>',
'<div class="inline fields">',
'<div class="field">',
'<input type="text" name="nombres" id="nombres" placeholder="Nombre(s)" />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_paterno" placeholder="Apellido Paterno" />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_materno" placeholder="Apellido Materno" />',
'</div>',
'<div class="field">',
'<input type="text" name="nombres" id="nombres" placeholder="Nombre(s)" required />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_paterno" placeholder="Apellido Paterno" required />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_materno" placeholder="Apellido Materno" required />',
'</div>',
'</div>',
'<label for="calle">Dirección</label>',
'<div class="inline fields">',
'<div class="eight wide field">',
'<input type="text" name="calle" id="calle" size="16" placeholder="Calle" />',
'</div>',
'<div class="field">',
'<input type="text" name="numero" size="5" placeholder="Número" />',
'</div>',
'<div class="field">',
'<input type="text" name="extra" placeholder="Otros Detalles" />',
'</div>',
'<div class="eight wide field">',
'<input type="text" name="calle" id="calle" size="16" placeholder="Calle" required />',
'</div>',
'<div class="field">',
'<input type="text" name="numero" size="5" placeholder="Número" required />',
'</div>',
'<div class="field">',
'<input type="text" name="extra" placeholder="Otros Detalles" />',
'</div>',
'</div>',
'<div class="inline fields">',
'<div class="two wide field"></div>',
'<div class="four wide field">',
'<div class="ui fluid search selection dropdown" id="comuna">',
'<input type="hidden" name="comuna" />',
'<div class="default text">Comuna</div>',
'<div class="menu"></div>',
'</div>',
'</div>',
'<div class="six wide field">',
'<div class="ui fluid search selection dropdown" id="region">',
'<input type="hidden" name="region" />',
'<div class="default text">Región</div>',
'<div class="menu">',
...regiones,
'</div>',
'</div>',
'<div class="two wide field"></div>',
'<div class="four wide field">',
'<div class="ui fluid search selection dropdown" id="comuna">',
'<input type="hidden" name="comuna" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Comuna</div>',
'<div class="menu"></div>',
'</div>',
'</div>',
'<div class="six wide field">',
'<div class="ui fluid search selection dropdown" id="region">',
'<input type="hidden" name="region" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Región</div>',
'<div class="menu">',
...regiones,
'</div>',
'</div>',
'</div>',
'</div>',
'<label>Otros Datos</label>',
'<div class="inline fields">',
'<div class="four wide field">',
'<input type="email" name="email" placeholder="Email" />',
'</div>',
'<div class="field">',
'<input type="text" name="telefono" placeholder="Teléfono" />',
'</div>',
'</div>'
]
return lines.join("\n")
}
activate() {
new Rut({id: '#rut', alert_id: '#alert_rut', valid: this.valid})
new RutHandler({id: '#rut', alert_id: '#alert_rut', valid: this.valid})
const comuna = new Comuna('#comuna')
new Region({id: '#region', comuna})
}
@ -416,7 +432,7 @@
const lines = [
'<label for="rut">RUT Sociedad</label>',
'<div class="inline field">',
'<input type="text" id="rut_sociedad" name="rut_sociedad" />',
'<input type="text" id="rut_sociedad" name="rut_sociedad" required />',
'<span class="ui error message" id="alert_rut_sociedad">',
'<i class="exclamation triangle icon"></i>',
'RUT Inválido',
@ -424,15 +440,15 @@
'</div>',
'<label for="razon_social">Razón Social</label>',
'<div class="field">',
'<input type="text" name="razon_social" id="razon_social" />',
'<input type="text" name="razon_social" id="razon_social" required />',
'</div>',
'<label for="calle_comercial">Dirección Comercial</label>',
'<div class="inline fields">',
'<div class="eight wide field">',
'<input type="text" name="calle_comercial" id="calle_comercial" size="16" />',
'<input type="text" name="calle_comercial" id="calle_comercial" size="16" required />',
'</div>',
'<div class="field">',
'<input type="text" name="numero_comercial" size="5" />',
'<input type="text" name="numero_comercial" size="5" required />',
'</div>',
'<div class="field">',
'<input type="text" name="extra_comercial" />',
@ -443,6 +459,7 @@
'<div class="four wide field">',
'<div class="ui fluid search selection dropdown" id="comuna_sociedad">',
'<input type="hidden" name="comuna_sociedad" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Comuna</div>',
'<div class="menu"></div>',
'</div>',
@ -450,6 +467,7 @@
'<div class="six wide field">',
'<div class="ui fluid search selection dropdown" id="region_sociedad">',
'<input type="hidden" name="region_sociedad" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Región</div>',
'<div class="menu">',
...regiones,
@ -458,11 +476,13 @@
'</div>',
'</div>',
'<div>Representante Legal</div>',
'<div class="ui divider"></div>',
this.persona.draw()
]
return [lines.join("\n"), this.persona.draw()].join("\n")
return lines.join("\n")
}
activate() {
new Rut({id: '#rut_sociedad', alert_id: '#alert_rut_sociedad'})
new RutHandler({id: '#rut_sociedad', alert_id: '#alert_rut_sociedad'})
const comuna = new Comuna('#comuna_sociedad')
new Region({id: '#region_sociedad', comuna})
this.persona.activate()
@ -477,9 +497,10 @@
draw() {
let lines = [
this.persona.draw(),
'<div class="ui divider"></div>',
'<label for="rut">RUT Otro</label>',
'<div class="inline field">',
'<input type="text" id="rut_otro" name="rut_otro" />',
'<input type="text" id="rut_otro" name="rut_otro" required />',
'<span class="ui error message" id="alert_rut_otro">',
'<i class="exclamation triangle icon"></i>',
'RUT Inválido',
@ -488,22 +509,22 @@
'<label for="nombres_otro">Nombre Otro</label>',
'<div class="inline fields">',
'<div class="field">',
'<input type="text" name="nombres_otro" id="nombres_otro" />',
'<input type="text" name="nombres_otro" id="nombres_otro" required />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_paterno_otro" />',
'<input type="text" name="apellido_paterno_otro" required />',
'</div>',
'<div class="field">',
'<input type="text" name="apellido_materno_otro" />',
'<input type="text" name="apellido_materno_otro" required />',
'</div>',
'</div>',
'<label for="calle_otro">Dirección Otro</label>',
'<div class="inline fields">',
'<div class="eight wide field">',
'<input type="text" name="calle_otro" id="calle_otro" size="16" />',
'<input type="text" name="calle_otro" id="calle_otro" size="16" required />',
'</div>',
'<div class="field">',
'<input type="text" name="numero_otro" size="5" />',
'<input type="text" name="numero_otro" size="5" required />',
'</div>',
'<div class="field">',
'<input type="text" name="extra_otro" />',
@ -514,6 +535,7 @@
'<div class="four wide field">',
'<div class="ui fluid search selection dropdown" id="comuna_otro">',
'<input type="hidden" name="comuna_otro" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Comuna</div>',
'<div class="menu"></div>',
'</div>',
@ -521,19 +543,29 @@
'<div class="six wide field">',
'<div class="ui fluid search selection dropdown" id="region_otro">',
'<input type="hidden" name="region_otro" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Región</div>',
'<div class="menu">',
...regiones,
'</div>',
'</div>',
'</div>',
'</div>',
'<label>Otros Datos para Otro</label>',
'<div class="inline fields">',
'<div class="four wide field">',
'<input type="email" name="email_otro" placeholder="Email" />',
'</div>',
'<div class="field">',
'<input type="text" name="telefono_otro" placeholder="Teléfono" />',
'</div>',
'</div>'
]
return lines.join("\n")
}
activate() {
this.persona.activate()
new Rut({id: '#rut_otro', alert_id: '#alert_rut_otro'})
new RutHandler({id: '#rut_otro', alert_id: '#alert_rut_otro'})
const comuna = new Comuna('#comuna_otro')
new Region({id: '#region_otro', comuna})
}
@ -596,6 +628,8 @@
parent.find("[name='extra']").val(data.propietario.direccion.extra)
parent.find('#region').dropdown('set selected', data.propietario.direccion.comuna.provincia.region.id)
parent.find('#comuna').dropdown('set selected', data.propietario.direccion.comuna.id)
parent.find("[name='email']").val(data.propietario.email)
parent.find("[name='telefono']").val(data.propietario.telefono)
if (data.propietario.representante !== '') {
document.getElementById(this.ids.tipo).trigger('check')
@ -634,6 +668,7 @@
$(this.ids.proyecto).dropdown({
fireOnInit: true,
forceSelection: true,
onChange: (value, text, $choice) => {
this.data.id = value
this.ids.buttons.forEach(button => {
@ -641,8 +676,10 @@
})
this.reset()
this.fetch().unidades().then(() => {
this.ids.buttons.forEach(button => {
$(button).prop('disabled', false)
Object.keys(this.unidades).forEach(name => {
if (this.unidades[name].length > 0) {
$(`#agregar_${name}`).prop('disabled', false)
}
})
})
}
@ -650,10 +687,11 @@
const buttons = [
'departamento',
'estacionamiento',
'bodega'
'bodega',
'terraza'
]
buttons.forEach(name => {
const id = '#agregar_' + name
const id = `#agregar_${name}`
this.ids.buttons.push(id)
$(id).click(event => {
this.add(name)
@ -770,7 +808,9 @@
}
$(document).ready(() => {
$('#fecha_venta_calendar').calendar(calendar_date_options)
const cdo = structuredClone(calendar_date_options)
cdo['maxDate'] = new Date()
$('#fecha_venta_calendar').calendar(cdo)
new Propietario({id: '#propietario', id_tipo: 'persona_propietario', id_cantidad: 'cantidad_propietario'})
new Proyecto({unidades_id: '#unidades', proyecto_id: '#proyecto'})
@ -789,7 +829,22 @@
$('#add_form').submit(event => {
event.preventDefault()
const button = $(event.currentTarget).find(".ui.button[type='submit']")
button.prop('disabled', true)
button.css('cursor', 'wait')
const body = new FormData(event.currentTarget)
const unidades = event.currentTarget.querySelectorAll("[name^='unidad']")
const emptyUnidades = []
unidades.forEach(unidad => {
if (unidad.value.trim() === '') {
emptyUnidades.push(unidad)
body.delete(unidad.name)
}
})
if (unidades.length === emptyUnidades.length) {
alert("No hay unidades seleccionadas")
return;
}
const uri = '{{$urls->api}}/ventas/add'
return fetchAPI(uri, {method: 'post', body}).then(response => {
if (!response) {
@ -800,6 +855,8 @@
window.location = '{{$urls->base}}/venta/' + data.venta_id
return true
}
button.prop('disabled', false)
button.css('cursor', 'pointer')
showErrors(data.errors)
return false
})

View File

@ -25,7 +25,7 @@
@endpush
@push('page_scripts')
<script type="text/javascript">
<script>
class Cierre {
id
proyecto_id

View File

@ -120,7 +120,7 @@
@endsection
@push('page_scripts')
<script type="text/javascript">
<script>
function action(action, cierre_id) {
const url = '{{$urls->base}}/api/cierre/' + cierre_id + '/' + action
console.debug(url)

View File

@ -65,7 +65,7 @@
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script type="text/javascript">
<script>
$(document).ready(() => {
const cuotas_tables = new DataTable('#cuotas', {
language: {

View File

@ -66,7 +66,7 @@
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script type="text/javascript">
<script>
$(document).ready(() => {
const cuotas_tables = new DataTable('#cuotas', {
language: {

View File

@ -54,22 +54,23 @@
}
$(document).ready(() => {
const url = '{{$urls->api}}/ventas/pago/{{$venta->resciliacion()->id}}'
let old = new Date({{$venta->resciliacion()->fecha->format('Y')}},
{{$venta->resciliacion()->fecha->format('n')}}-1, {{$venta->resciliacion()->fecha->format('j')}})
let old = new Date(Date.parse('{{$venta->resciliacion()?->fecha->format('Y-m-d') ?? $venta->currentEstado()->fecha->format('Y-m-d') ?? $venta->fecha->format('Y-m-d')}}') + 24 * 60 * 60 * 1000)
calendar_date_options['initialDate'] = old
calendar_date_options['onChange'] = function(date, text, mode) {
if (date.getTime() === old.getTime()) {
return
}
const body = new FormData()
body.set('fecha', date.toISOString())
const fecha = new Date(date.getTime())
fecha.setDate(fecha.getDate() - 1)
body.set('fecha', fecha.toISOString())
$('#loading-spinner-fecha').show()
fetchAPI(url, {method: 'post', body}).then(response => {
APIClient.fetch(url, {method: 'post', body}).then(response => {
$('#loading-spinner-fecha').hide()
if (!response) {
return
}
old = date
old = new Date(date.getTime())
alertResponse('Fecha cambiada correctamente.')
})
}
@ -79,7 +80,7 @@
const body = new FormData()
body.set('valor', val)
$('#loading-spinner-devolucion').show()
fetchAPI(url, {method: 'post', body}).then(response => {
APIClient.fetch(url, {method: 'post', body}).then(response => {
$('#loading-spinner-devolucion').hide()
if (!response) {
return
@ -90,7 +91,7 @@
$('#eliminar_desistimiento').click(event => {
const url = '{{$urls->api}}/venta/{{$venta->id}}/desistir/eliminar'
$('#loading-spinner-eliminar').show()
fetchAPI(url).then(response => {
APIClient.fetch(url).then(response => {
$('#loading-spinner-eliminar').hide()
if (!response) {
alertResponse('No se pudo eliminar el desistimiento', {color: 'red', icon: 'triangle exclamation'})

View File

@ -6,15 +6,26 @@
@section('venta_content')
<div class="ui list">
<div class="item">
<div class="header">Valor Pagado</div>
<div class="content">
{{$format->pesos($venta->formaPago()->pie->pagado('pesos'))}}
<div class="ui left pointing small label">
{{$format->number($venta->formaPago()->pie->pagado() / $venta->valor * 100)}}% de la venta
@if (isset($venta->formaPago()->pie))
<div class="item">
<div class="header">Valor Pagado</div>
<div class="content">
{{$format->pesos($venta->formaPago()->pie->pagado('pesos'))}}
<div class="ui left pointing small label">
{{$format->number($venta->formaPago()->pie->pagado() / $venta->valor * 100)}}% de la venta
</div>
</div>
</div>
</div>
@else
<div class="item">
<div class="ui compact warning message">
<div class="content">
<i class="exclamation triangle icon"></i>
No tiene valor pagado
</div>
</div>
</div>
@endif
<div class="item">
<div class="header">
Multa Estandar

View File

@ -29,7 +29,7 @@
@endsection
@push('page_scripts')
<script type="text/javascript">
<script>
const editVenta = {
getMonthsList() {
const formatter = new Intl.DateTimeFormat('es-CL', {month: 'long'})

View File

@ -23,43 +23,45 @@
</div>
</div>
</div>
@if ($venta->formaPago()->pie->reajuste === null)
<h4 class="ui header optional" data-name="reajuste">Reajuste <i class="small plus icon"></i></h4>
<div class="fields optional" id="reajuste">
<div class="field">
<label for="valor_reajuste">Valor [$]</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="text" name="valor_reajuste" id="valor_reajuste" />
@if ($venta->formaPago()->pie !== null)
@if ($venta->formaPago()->pie->reajuste === null)
<h4 class="ui header optional" data-name="reajuste">Reajuste <i class="small plus icon"></i></h4>
<div class="fields optional" id="reajuste">
<div class="field">
<label for="valor_reajuste">Valor [$]</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="text" name="valor_reajuste" id="valor_reajuste" />
</div>
</div>
</div>
<div class="field">
<label for="fecha_reajuste">Fecha</label>
<div class="ui calendar" id="fecha_reajuste">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha_reajuste" />
<div class="field">
<label for="fecha_reajuste">Fecha</label>
<div class="ui calendar" id="fecha_reajuste">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha_reajuste" />
</div>
</div>
</div>
</div>
</div>
@else
<h4 class="ui header" data-name="reajuste">Reajuste</h4>
<div class="fields" id="reajuste">
<div class="field">
<label for="valor_reajuste">Valor [$]</label>
<div class="ui left labeled disabled input">
<div class="ui basic label">$</div>
<input type="text" value="{{$format->number($venta->formaPago()->pie->reajuste->valor)}}" />
@else
<h4 class="ui header" data-name="reajuste">Reajuste</h4>
<div class="fields" id="reajuste">
<div class="field">
<label for="valor_reajuste">Valor [$]</label>
<div class="ui left labeled disabled input">
<div class="ui basic label">$</div>
<input type="text" value="{{$format->number($venta->formaPago()->pie->reajuste->valor)}}" />
</div>
</div>
<div class="field">
<label for="fecha_reajuste">Fecha</label>
<div class="ui disabled input">
<input type="text" value="{{$venta->formaPago()->pie->reajuste->fecha->format('d-m-Y')}}" />
</div>
</div>
</div>
<div class="field">
<label for="fecha_reajuste">Fecha</label>
<div class="ui disabled input">
<input type="text" value="{{$venta->formaPago()->pie->reajuste->fecha->format('d-m-Y')}}" />
</div>
</div>
</div>
@endif
@endif
@if (!isset($venta->formaPago()->escritura))
<h4 class="ui header optional" data-name="pago">Pago en Escritura <i class="small plus icon"></i></h4>
@ -249,16 +251,27 @@
subsidio.watch()
$('#escriturar_form').submit(event => {
event.preventDefault()
$('.ui.button').addClass('loading')
const url = '{{$urls->api}}/venta/{{$venta->id}}/escriturar'
const body = new FormData(event.currentTarget)
body.set('fecha', $('#fecha').calendar('get date').toISOString())
if (body.get('fecha_pago') !== '') {
body.set('fecha_pago', $('#fecha_pago').calendar('get date').toISOString())
const fecha = $('#fecha').calendar('get date')
body.set('fecha', [fecha.getFullYear(), fecha.getMonth()+1, fecha.getDate()].join('-'))
const $fechaPago = $('#fecha_pago')
if ($fechaPago.length > 0) {
const fechaPago = $fechaPago.calendar('get date')
if (fechaPago !== null) {
body.set('fecha_pago', [fechaPago.getFullYear(), fechaPago.getMonth()+1, fechaPago.getDate()].join('-'))
}
}
if (body.get('fecha_reajuste') !== '') {
body.set('fecha_reajuste', $('#fecha_reajuste').calendar('get date').toISOString())
const $fechaReajuste = $('#fecha_reajuste')
if ($fechaReajuste.length > 0) {
const fechaReajuste = $fechaReajuste.calendar('get date')
if (fechaReajuste !== null) {
body.set('fecha_reajuste', [fechaReajuste.getFullYear(), fechaReajuste.getMonth()+1, fechaReajuste.getDate()].join('-'))
}
}
fetchAPI(url, {method: 'post', body}).then(response => {
$('.ui.button').removeClass('loading')
if (response.ok) {
return response.json()
}

View File

@ -0,0 +1,166 @@
@extends('ventas.base')
@section('venta_subtitle')
Cuotas - Abono a Escritura
@endsection
@section('venta_content')
<table class="ui table">
<thead>
<tr>
<th rowspan="2">#</th>
<th rowspan="2">Fecha</th>
<th colspan="2">Valor</th>
<th rowspan="2" colspan="2">Estado</th>
<th class="right aligned" rowspan="2">
<button class="ui tertiary green icon button" id="add_button">
<i class="plus icon"></i>
</button>
</th>
</tr>
<tr>
<th class="right aligned">$</th>
<th class="right aligned">UF</th>
</tr>
</thead>
<tbody id="cuotas">
@foreach ($cuotas as $cuota)
<tr data-id="{{$cuota->id}}">
<td class="numero" data-value="{{$cuota->numero}}">{{$cuota->numero}}</td>
<td class="fecha" data-value="{{$cuota->pago->fecha->format('Y-m-d')}}">{{$cuota->pago->fecha->format('d-m-Y')}}</td>
<td class="valor right aligned" data-value="{{$cuota->pago->valor}}">{{$format->pesos($cuota->pago->valor)}}</td>
<td class="valor_uf right aligned" data-value="{{$cuota->pago->valor()}}">{{$format->ufs($cuota->pago->valor())}}</td>
<td class="estado{{$cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'no pagado' ? ' warning' :
($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'abonado' ? ' positive' : '')}}"
data-value="{{$cuota->pago->currentEstado->tipoEstadoPago->id}}">
{{ucwords($cuota->pago->currentEstado->tipoEstadoPago->descripcion)}}
@if (in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['abonado', 'depositado']))
<br />
{{$cuota->pago->currentEstado->fecha->format('d-m-Y')}}
@endif
</td>
<td class="collapsing">
@if (in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['depositado', 'no pagado']))
<form class="ui compact form avance_pago_form" data-id="{{$cuota->id}}"
data-pago="{{$cuota->pago->id}}"
data-estado="{{$cuota->pago->currentEstado->tipoEstadoPago->descripcion}}">
<div class="inline field">
<div class="ui calendar">
<div class="ui icon input">
<input type="text" name="fecha_avance_pago{{$cuota->id}}" />
<i class="calendar icon"></i>
</div>
</div>
<button class="ui tertiary green icon button accept_button" data-id="{{$cuota->id}}"><i class="check icon"></i></button>
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado')
<button class="ui tertiary red icon button cancel_button" data-id="{{$cuota->id}}"><i class="remove icon"></i></button>
@endif
</div>
</form>
@endif
</td>
<td class="right aligned">
<button class="ui tertiary icon button edit_button" data-id="{{$cuota->id}}">
<i class="edit icon"></i>
</button>
<button class="ui tertiary red icon button remove_button" data-id="{{$cuota->id}}">
<i class="remove icon"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@include('ventas.escrituras.abono.cuotas.add_modal')
@include('ventas.escrituras.abono.cuotas.edit_modal')
@endsection
@push('page_scripts')
<script>
$(document).ready(function () {
const addModal = new AddModal({
modal: '#add_cuota_modal',
form: 'add_cuota_form',
fecha: '#add_fecha',
})
const editModal = new EditModal({
table: 'cuotas',
modal: '#edit_cuota_modal',
form: 'edit_cuota_form',
fecha: '#edit_fecha',
estado: '#edit_estado'
})
document.getElementById('add_button').addEventListener('click', clickEvent => {
addModal.draw()
})
Array.from(document.getElementsByClassName('edit_button')).forEach(button => {
button.addEventListener('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
editModal.getData({cuota_id: id})
})
})
Array.from(document.getElementsByClassName('remove_button')).forEach(button => {
button.addEventListener('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
const url = `{{$urls->api}}/venta/{{$venta->id}}/cuota/${id}`
const method = 'delete'
console.debug(url)
})
})
Array.from(document.getElementsByClassName('avance_pago_form')).forEach(form => {
form.addEventListener('submit', submitEvent => {
submitEvent.preventDefault()
return false
})
const cdo = structuredClone(calendar_date_options)
cdo['initialDate'] = new Date()
cdo['maxDate'] = new Date()
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado')
cdo['initialDate'] = new Date('{{$cuota->pago->currentEstado->fecha->format('Y-m-d')}}')
@endif
$(form).find('.ui.calendar').calendar(cdo)
})
Array.from(document.getElementsByClassName('accept_button')).forEach(button => {
button.addEventListener('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
const method = 'post'
const form = Array.from(document.getElementsByClassName('avance_pago_form')).filter(form => parseInt(form.dataset.id) === id)[0]
const pago = form.dataset.pago
const estado = form.dataset.estado
const newEstado = estado === 'depositado' ? 'abonar' : 'depositar'
const url = `{{$urls->api}}/ventas/pago/${pago}/${newEstado}`
const body = new FormData()
const fecha = $(form).find('.ui.calendar').calendar('get date')[0]
body.set('fecha', fecha.getFullYear() + '-' + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + fecha.getDate().toString().padStart(2, '0'))
APIClient.fetch(url, {method, body}).then(response => {
if (response.ok) {
return response.json().then(json => {
if (json.success) {
window.location.reload()
}
})
}
})
})
})
Array.from(document.getElementsByClassName('cancel_button')).forEach(button => {
button.addEventListener('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('id')
const method = 'post'
const form = Array.from(document.getElementsByClassName('avance_pago_form')).filter(form => parseInt(form.dataset.id) === id)[0]
const pago = form.dataset.pago
const url = `{{$urls->api}}/ventas/pago/${pago}/devolver`
const body = new FormData()
const fecha = $(form).find('.ui.calendar').calendar('get date')[0]
body.set('fecha', fecha.getFullYear() + '-' + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + fecha.getDate().toString().padStart(2, '0'))
console.debug(url, body)
})
})
})
</script>
@endpush

View File

@ -0,0 +1,87 @@
<div class="ui modal" id="add_cuota_modal">
<div class="header">
Agregar Cuota
</div>
<div class="content">
<form class="ui form" id="add_cuota_form">
<input type="hidden" name="venta_id" value="{{$venta->id}}" />
<div class="two wide field">
<label>Número</label>
<input type="text" name="numero" />
</div>
<div class="three wide field">
<label>Fecha</label>
<div class="ui calendar" id="add_fecha">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha" />
</div>
</div>
</div>
<div class="three wide field">
<label>Valor $</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="text" name="valor" />
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Agregar
<i class="add icon"></i>
</div>
</div>
</div>
@push('page_scripts')
<script>
class AddModal {
props
constructor(props) {
this.setup(props)
}
draw() {
$(this.props.modal).modal('show')
}
save() {
const form = document.getElementById(this.props.form)
const body = new FormData(form)
const fecha = $(this.props.fecha).calendar('get date')
body.set('fecha', fecha.getFullYear() + '-' + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + fecha.getDate().toString().padStart(2, '0'))
const url = `{{$urls->api}}/venta/{{$venta->id}}/escritura/cuotas/add`
const method = 'post'
APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (json.success) {
window.location.reload()
}
})
})
}
setup(ids) {
this.props = ids
$(this.props.modal).modal({
onApprove: () => {
this.save()
}
})
$(this.props.form).submit(event => {
event.preventDefault()
this.save()
return false
})
$(this.props.fecha).calendar(calendar_date_options)
}
}
</script>
@endpush

View File

@ -0,0 +1,130 @@
<div class="ui modal" id="edit_cuota_modal">
<div class="header">
Editar Cuota <span class="numero"></span>
</div>
<div class="content">
<form class="ui form" id="edit_cuota_form">
<input type="hidden" name="id" />
<div class="two wide field">
<label>Número</label>
<input type="text" name="numero" />
</div>
<div class="three wide field">
<label>Fecha</label>
<div class="ui calendar" id="edit_fecha">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha" />
</div>
</div>
</div>
<div class="three wide field">
<label>Valor $</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="text" name="valor" />
</div>
</div>
<div class="three wide field">
<label>Estado</label>
<div class="ui selection search dropdown" id="edit_estado">
<i class="dropdown icon"></i>
<input type="hidden" name="tipo_estado_id" />
<div class="default text">Estado</div>
<div class="menu">
@foreach($estados as $estado)
<div class="item" data-value="{{$estado->id}}">{{$estado->descripcion}}</div>
@endforeach
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui negative button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Editar
<i class="edit icon"></i>
</div>
</div>
</div>
@push('page_scripts')
<script>
class EditModal {
props
data
constructor(props) {
this.setup(props)
this.data = null
}
getData({cuota_id}) {
const table = document.getElementById(this.props.table)
const row = table.querySelector(`tr[data-id='${cuota_id}']`)
const fecha = row.querySelector('.fecha').dataset.value.split('-')
this.data = {
id: cuota_id,
numero: row.querySelector('.numero').dataset.value,
fecha: new Date(fecha[0], fecha[1] - 1, fecha[2]),
valor: row.querySelector('.valor').dataset.value,
estado: row.querySelector('.estado').dataset.value
}
this.draw()
}
draw() {
const form = document.getElementById(this.props.form)
form.querySelector('input[name="id"]').value = this.data.id
form.querySelector('input[name="numero"]').value = this.data.numero
$(this.props.fecha).calendar('set date', this.data.fecha)
form.querySelector('input[name="valor"]').value = this.data.valor
$(this.props.estado).dropdown('set selected', this.data.estado)
$(this.props.modal).find('.header .numero').text(this.data.numero)
$(this.props.modal).modal('show')
}
save() {
const form = document.getElementById(this.props.form)
const body = new FormData(form)
const fecha = $(this.props.fecha).calendar('get date')
body.set('fecha', fecha.getFullYear() + '-' + (fecha.getMonth() + 1).toString().padStart(2, '0') + '-' + fecha.getDate().toString().padStart(2, '0'))
body.set('tipo_estado_id', $(this.props.estado).dropdown('get value'))
const url = `{{$urls->api}}/venta/{{$venta->id}}/escritura/cuota/${this.data.id}/edit`
const method = 'post'
APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (json.success) {
window.location.reload()
}
})
})
}
setup(ids) {
this.props = ids
$(this.props.modal).modal({
onApprove: () => {
this.save()
},
onHidden: () => {
this.data = null
}
})
$(this.props.form).submit(event => {
event.preventDefault()
this.save()
return false
})
$(this.props.fecha).calendar(calendar_date_options)
$(this.props.estado).dropdown()
}
}
</script>
@endpush

View File

@ -14,6 +14,7 @@
<p>Crédito {{$format->ufs($venta->formaPago()->credito->pago->valor())}}</p>
@endif
</div>
<a href="{{$urls->base}}/venta/{{$venta->id}}/escritura/cuotas" class="ui small green button"><i class="plus icon"></i> Agregar Cuotas</a>
<form class="ui form" id="add_form">
<div class="three wide field">
<label for="fecha">Fecha</label>

View File

@ -6,7 +6,12 @@
@section('venta_content')
<div class="ui segment">
El departamento {{$venta->propiedad()->departamentos()[0]->descripcion}}:<br />
@if (count($venta->propiedad()->departamentos()) > 0)
El departamento {{$venta->propiedad()->departamentos()[0]->descripcion}}:<br />
@else
La unidad {{ ucwords($venta->propiedad()->principal()?->proyectoTipoUnidad->tipoUnidad->descripcion) }}
{{ $venta->propiedad()->principal()?->descripcion }}:<br />
@endif
@php
$estacionamientos = $venta->propiedad()->estacionamientos();
@endphp

View File

@ -5,6 +5,24 @@
@endsection
@section('venta_content')
@if (count($venta->formaPago()->cuotasAbono) > 0)
<a href="{{$urls->base}}/venta/{{$venta->id}}/escritura/cuotas" class="ui tertiary green button">Ver Cuotas</a>
<div class="ui compact segment">
<div class="header">
Cuotas
</div>
<div class="ui horizontal list">
<div class="item">
{{$format->pesos($venta->formaPago()->cuotasAbono('pesos'))}}
</div>
<div class="item">
{{$format->ufs($venta->formaPago()->cuotasAbono())}}
</div>
</div>
</div>
@else
<a href="{{$urls->base}}/venta/{{$venta->id}}/escritura/cuotas" class="ui small green button"><i class="plus icon"></i> Agregar Cuotas</a>
@endif
<form class="ui form" id="edit_form">
<div class="three wide field">
<label for="fecha">Fecha</label>
@ -15,6 +33,26 @@
</div>
</div>
</div>
<div class="three wide field">
<label for="valor">Valor</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="text" name="valor" id="valor" value="{{$venta->formaPago()->escritura->pago->valor}}" />
</div>
</div>
<div class="three wide field">
<label>Estado</label>
<div class="ui selection dropdown" id="estado">
<input type="hidden" name="estado" />
<div class="default text">Estado</div>
<i class="dropdown icon"></i>
<div class="menu">
@foreach($estados as $estado)
<div class="item" data-value="{{$estado->id}}">{{ucwords($estado->descripcion)}}</div>
@endforeach
</div>
</div>
</div>
<button class="ui button">Guardar</button>
</form>
@endsection
@ -24,23 +62,27 @@
function editEscritura() {
const url = '{{$urls->api}}/ventas/escritura/{{$venta->id}}/edit'
const data = new FormData()
data.set('venta', {{$venta->id}})
const fecha = $('#fecha').calendar('get date')
data.set('fecha', fecha.toISOString())
data.set('valor', $('#valor').val())
data.set('estado', $('#estado').dropdown('get value'))
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.edited) {
if (!json.success) {
return
}
window.location = '{{$urls->base}}/venta/{{$venta->id}}'
})
}
$(document).ready(() => {
calendar_date_options.initialDate = new Date({{$venta->currentEstado()->fecha->format('Y, m-1, j')}})
calendar_date_options.initialDate = new Date({{$venta->formaPago()->escritura->pago->currentEstado->fecha->format('Y, m-1, j')}})
$('#fecha').calendar(calendar_date_options)
const $estado = $('#estado')
$estado.dropdown()
$estado.dropdown('set selected', '{{$venta->formaPago()->escritura->pago->currentEstado->tipoEstadoPago->id}}')
$('#edit_form').submit(event => {
event.preventDefault()
editEscritura()

View File

@ -1,5 +1,14 @@
@extends('layout.base')
@push('page_styles')
<style>
#data {
margin-left: 1rem;
margin-right: 1rem;
}
</style>
@endpush
@section('page_content')
<div class="ui container">
<h2 class="ui header">Matriz Facturación</h2>
@ -24,7 +33,7 @@
@endforeach
</div>
</div>
<table class="ui table" id="data"></table>
<div id="data"></div>
@endsection
@include('layout.head.styles.datatables')
@ -33,53 +42,150 @@
@push('page_scripts')
<script>
const money = {
data: {
ufs: {},
ipcs: {}
},
ufs: {},
ipcs: {},
sent: {
uf: {},
ipc: {}
},
queue: {
size: 0,
array: () => {
const array = []
Object.entries(money.queue).forEach(([type, dates]) => {
if (['uf', 'ipc'].includes(type) === false) {
return
}
if (type === 'uf') {
Object.entries(dates).forEach(([dateString, ventas]) => {
const temp = []
ventas.forEach(venta => {
temp.push(venta)
})
array.push({
type,
date: dateString,
ventas: temp
})
})
return
}
Object.entries(dates).forEach(([dateString, data]) => {
const temp = []
data.ventas.forEach(venta => {
temp.push(venta)
})
array.push({
type,
start: data.start,
end: data.end,
ventas: temp
})
})
})
return array
}
},
enqueue() {
return {
uf: ({date, venta_id}) => {
const type = 'uf'
const data = {
date: [date.getFullYear(), date.getMonth()+1, date.getDate()].join('-'),
}
if (!(type in this.queue)) {
this.queue[type] = {}
}
if (!(data.date in this.queue[type])) {
this.queue[type][data.date] = []
}
if (this.queue[type][data.date].includes(data)) {
return
}
this.queue[type][data.date].push(venta_id)
this.queue['size'] ++
},
ipc: ({start, end, venta_id}) => {
const type = 'ipc'
let mult = 1
if (start > end) {
mult = -1
let temp = end
end = start
start = temp
}
const data = {
start: [start.getFullYear(), start.getMonth()+1, start.getDate()].join('-'),
end: [end.getFullYear(), end.getMonth()+1, end.getDate()].join('-'),
}
if (!(type in this.queue)) {
this.queue[type] = {}
}
if (!([data.start, data.end].join('|') in this.queue[type])) {
this.queue[type][[data.start, data.end].join('|')] = {
start: data.start,
end: data.end,
ventas: []
}
}
if (this.queue[type][[data.start, data.end].join('|')].ventas.includes(data)) {
return
}
this.queue[type][[data.start, data.end].join('|')].ventas.push({venta_id, mult})
this.queue['size'] ++
},
}
},
get() {
return {
uf: date => {
if (typeof this.sent.uf[date.toISOString()] !== 'undefined') {
return this.sent.uf[date.toISOString()]
}
const url = '{{$urls->api}}/money/uf'
const data = new FormData()
data.set('fecha', date.toISOString())
const options = {
method: 'post',
body: data
}
return this.sent.uf[date.toISOString()] = fetchAPI(url, options).then(response => {
if (response.ok) {
return response.json()
many: () => {
const url = '{{$urls->api}}/money/many'
const method = 'post'
const chunkSize = 100
const promises = []
for (let i = 0; i < this.queue.size; i += chunkSize) {
const chunk = this.queue.array().slice(i, i + chunkSize)
const missing = chunk.filter(data => {
if (data.type === 'uf') {
return !(data.date in this.data.ufs)
}
if (data.type === 'ipc') {
const dateKey = [data.start, data.end].join('|')
return !(dateKey in this.data.ipcs)
}
})
if (missing.length === 0) {
continue
}
}).then(json => {
return this.ufs[json.input.fecha] = json.uf
})
},
ipc: (end, start) => {
const dateKey = [start.getFullYear(), (start.getMonth()+1), end.getFullYear(), (end.getMonth()+1)].join('-')
if (typeof this.sent.ipc[dateKey] !== 'undefined') {
return this.sent.ipc[dateKey]
}
const url = '{{$urls->api}}/money/ipc'
const data = new FormData()
data.set('start', start.toISOString())
data.set('end', end.toISOString())
const options = {
method: 'post',
body: data
}
return this.sent.ipc[dateKey] = fetchAPI(url, options).then(response => {
if (response.ok) {
return response.json()
const body = new FormData()
body.set('dates', JSON.stringify(missing))
const options = {
method,
body
}
}).then(json => {
return this.ipcs[json.date_string] = json.ipc
})
promises.push(fetchAPI(url, options).then(response => {
if (!response) {
return
}
return response.json().then(json => {
json.values.forEach(data => {
if (data.type === 'uf') {
this.data.ufs[data.date] = data.value
}
if (data.type === 'ipc') {
const dateKey = [data.start, data.end].join('|')
this.data.ipcs[dateKey] = data.value
}
})
return json.values
})
}))
}
return Promise.all(promises)
}
}
}
@ -92,8 +198,7 @@
prorrateo
precio
constructor({id, tipo, descripcion, prorrateo, precio})
{
constructor({id, tipo, descripcion, prorrateo, precio}) {
this.id = id
this.tipo = tipo
this.descripcion = descripcion
@ -111,44 +216,13 @@
unidades
principal
constructor({id, precio, fecha, escritura}) {
constructor({id, precio, fecha, escritura, unidades, principal}) {
this.id = id
this.precio = precio
this.fecha = fecha
this.escritura = escritura
this.uf = 1
this.ipc = 1
this.unidades = []
}
get() {
return {
unidades: () => {
const url = '{{$urls->api}}/venta/' + this.id + '/unidades'
return fetchAPI(url).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
json.unidades.forEach(unidad => {
const tipo = unidad.proyecto_tipo_unidad.tipo_unidad.descripcion
const data = {
id: unidad.id,
tipo: tipo.charAt(0).toUpperCase() + tipo.slice(1),
descripcion: unidad.descripcion,
prorrateo: unidad.prorrateo,
precio: 0
}
if (unidad.current_precio !== null) {
data.precio = unidad.current_precio.valor
}
const u = new Unidad(data)
this.unidades.push(u)
})
this.principal = this.unidades.filter(unidad => unidad.tipo === 'Departamento')[0]
})
}
}
this.fecha = new Date(fecha)
this.escritura = new Date(escritura)
this.unidades = unidades
this.principal = principal
}
draw({tbody, valor_terreno}) {
@ -173,16 +247,17 @@
const dateFormatter = new Intl.DateTimeFormat('es-CL', {year: 'numeric', month: 'numeric', day: 'numeric'})
const pesosFormatter = new Intl.NumberFormat('es-CL', {style: 'currency', currency: 'CLP'})
const ufFormatter = new Intl.NumberFormat('es-CL', {minimumFractionDigits: 2, maximumFractionDigits: 2})
const venta = this.unidades[0].descripcion
const venta = this.principal.descripcion
const cantidad = this.unidades.length
this.unidades.forEach(unidad => {
const valor = (unidad.valor > 0) ? unidad.valor : unidad.precio
const values = [
venta,
'<a href="{{$urls->base}}/ventas/factura/' + this.id + '">' + venta + '</a>',
cantidad,
unidad.tipo,
unidad.descripcion,
dateFormatter.format(this.fecha),
ufFormatter.format(unidad.precio) + ' UF',
ufFormatter.format(valor) + ' UF',
unidad.prorrateo,
'',
'',
@ -195,22 +270,23 @@
''
]
if (this.escritura !== null) {
const descuento = valor_terreno * (1 + this.ipc / 100) * unidad.prorrateo
const precio_venta = unidad.precio * this.uf
const ipc = (this.ipc >= 0) ? (1 + this.ipc) : 1 / (1 + this.ipc)
const descuento = valor_terreno * ipc * unidad.prorrateo
const precio_venta = valor * this.uf
const precio_bruto = precio_venta - descuento
const precio_neto = precio_bruto / 1.19
const iva = precio_bruto - precio_neto
let i = 7
values[i++] = pesosFormatter.format(descuento)
values[i++] = pesosFormatter.format(precio_venta)
values[i++] = pesosFormatter.format(precio_bruto)
values[i++] = pesosFormatter.format(iva)
values[i++] = pesosFormatter.format(precio_neto)
values[i++] = (iva / precio_venta * 100).toFixed(2) + '%'
values[i++] = (!isNaN(descuento)) ? pesosFormatter.format(descuento) : '<div class="ui active inline loader"></div>'
values[i++] = (!isNaN(precio_venta)) ? pesosFormatter.format(precio_venta) : ''
values[i++] = (!isNaN(precio_bruto)) ? pesosFormatter.format(precio_bruto) : ''
values[i++] = (!isNaN(iva)) ? pesosFormatter.format(iva) : ''
values[i++] = (!isNaN(precio_neto)) ? pesosFormatter.format(precio_neto) : ''
values[i++] = (!isNaN(iva) && !isNaN(precio_venta)) ? (iva / precio_venta * 100).toFixed(2) + '%' : ''
values[i++] = dateFormatter.format(this.escritura)
values[i++] = ufFormatter.format(this.uf)
values[i++] = (this.ipc).toFixed(2) + '%'
values[i++] = (!isNaN(this.uf)) ? ufFormatter.format(this.uf) : ''
values[i++] = (!isNaN(this.ipc)) ? (ufFormatter.format(this.ipc >= 0 ? this.ipc * 100 : -this.ipc * 100) + '%') : ''
}
const row = $('<tr></tr>')
values.forEach(value => {
@ -226,74 +302,144 @@
ids: {},
selected: 0,
data: JSON.parse('{!! json_encode($proyectos) !!}'),
sent: false,
queues: {
uf: {},
ipc: {}
},
ufs: {},
ipcs: {},
table: null,
get: function() {
return {
ventas: () => {
if (this.sent) {
return
}
this.sent = true
const url = '{{$urls->api}}/ventas/facturacion/proyecto/' + this.selected
this.draw().loading()
return fetchAPI(url).then(response => {
if (response.ok) {
return response.json()
if (!response) {
return
}
}).then(json => {
const idx = this.data.findIndex(proyecto => proyecto.id === json.proyecto_id)
const fecha_terreno = new Date(this.data[idx].terreno.date)
this.data[idx]['ventas'] = []
const ventas = []
json.ventas.forEach(venta => {
const data = {
id: venta.id,
precio: venta.valor,
fecha: new Date(venta.fecha),
escritura: null
return response.json().then(json => {
const idx = this.data.findIndex(proyecto => proyecto.id === json.proyecto_id)
this.data[idx]['ventas'] = []
const ventas = []
const chunkSize = 100
for (let i = 0; i < json.ventas.length; i += chunkSize) {
const chunk = json.ventas.slice(i, i + chunkSize).map(venta => venta.id)
ventas.push(this.get().chunk({idx, chunk}))
}
if (['escriturando'].includes(venta.current_estado.tipo_estado_venta.descripcion)) {
data.escritura = new Date(venta.current_estado.fecha)
}
const v = new Venta(data)
const promises = []
if (v.escritura !== null) {
promises.push(money.get().uf(v.escritura).then(uf => {
v.uf = uf
}))
promises.push(money.get().ipc(v.escritura, fecha_terreno).then(ipc => {
v.ipc = ipc
}))
}
promises.push(v.get().unidades())
const promise = Promise.all(promises).then(() => {
this.data[idx].ventas.push(v)
Promise.all(ventas).then(() => {
this.draw().ventas(idx)
this.sent = false
money.get().many().then(response => {
response.forEach(dates => {
dates.forEach(values => {
values.ventas.forEach(venta => {
let vidx = 0
if (values.type === 'uf') {
vidx = this.data[idx].ventas.findIndex(v => v.id === venta)
this.data[idx].ventas[vidx].uf = values.value
return
}
if (values.type === 'ipc') {
vidx = this.data[idx].ventas.findIndex(v => v.id === venta.venta_id)
this.data[idx].ventas[vidx].ipc = values.value * venta.mult
}
})
})
})
this.draw().ventas(idx)
})
})
ventas.push(promise)
})
Promise.all(ventas).then(() => {
this.data[idx].ventas.sort((a, b) => parseInt(a.principal.descripcion) - parseInt(b.principal.descripcion))
this.draw().ventas(idx)
})
})
},
chunk: ({idx, chunk}) => {
const url = '{{$urls->api}}/ventas/facturacion/get'
const method = 'post'
const body = new FormData()
body.set('ventas', chunk)
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return response
}
return response.json().then(json => {
json.ventas.forEach(venta => {
this.add().venta({proyecto_idx: idx, venta})
})
});
})
},
}
},
add() {
return {
venta: ({proyecto_idx, venta}) => {
const v = new Venta(venta)
this.data[proyecto_idx].ventas.push(v)
const fecha_terreno = (typeof this.data[proyecto_idx].terreno.fecha === 'undefined') ? new Date() : new Date(this.data[proyecto_idx].terreno.fecha)
if (v.escritura !== null) {
money.enqueue().uf({date: v.escritura, venta_id: v.id})
money.enqueue().ipc({start: v.escritura, end: fecha_terreno, venta_id: v.id})
}
},
}
},
register() {
return {
uf: ({dateString, venta_id}) => {
if (!Object.hasOwn(this.queues.uf, dateString)) {
this.queues.uf[dateString] = []
}
this.queues.uf[dateString].push(venta_id)
},
ipc: ({dateString, venta_id}) => {
if (!Object.hasOwn(this.queues.ipc, dateString)) {
this.queues.ipc[dateString] = []
}
this.queues.ipc[dateString].push(venta_id)
}
}
},
draw: function() {
return {
proyectos: () => {
$(this.ids.title).html('Proyectos')
if (this.table !== null) {
this.table.clear()
this.table.destroy()
$(this.ids.data).html('')
}
$(this.ids.data).hide()
$(this.ids.proyectos).find('.item').css('cursor', 'pointer')
$(this.ids.proyectos).show()
},
ventas: proyecto_idx => {
const table = $(this.ids.data)
$(this.ids.title).html(this.data[proyecto_idx].descripcion)
const parent = $(this.ids.data)
if (this.table !== null) {
this.table.clear().destroy()
this.table.clear()
this.table.destroy()
this.table = null
parent.html('')
}
const table = $('<table></table>').addClass('ui compact table')
table.append(this.draw().thead())
table.append(this.draw().tbody(proyecto_idx))
table.show()
parent.show()
parent.html(table)
$(this.ids.proyectos).hide()
if (this.table === null) {
this.table = table.DataTable()
const dtD = structuredClone(datatables_defaults)
dtD['pageLength'] = 100
dtD['order'] = [[0, 'asc']]
this.table = table.DataTable(dtD)
}
},
thead: () => {
@ -331,6 +477,9 @@
venta.draw({tbody, valor_terreno: this.data[proyecto_idx].terreno.valor})
})
return tbody
},
loading: () => {
$(this.ids.proyectos).find('.item').css('cursor', 'wait')
}
}
},
@ -369,6 +518,7 @@
}
$(document).ready(() => {
proyectos.setup({ids: {
title: '#list_title',
proyectos: '#proyectos',
data: '#data',
buttons: {

View File

@ -13,439 +13,214 @@
{{$venta->propiedad()->summary()}}
</a>
</h2>
<div class="ui very basic segment">
Valor Venta: {{$format->ufs($venta->valor)}}
</div>
<form id="venta_form" class="ui form">
<div class="two wide field">
<label for="proporcion">Proporción Factura</label>
<div class="ui right labeled input">
<input type="number" name="proporcion" id="proporcion" value="100" max="100" min="0"/>
<div class="ui basic icon label">
<i class="percent icon"></i>
</div>
</div>
</div>
<div class="fields">
@foreach ($venta->propiedad()->unidades as $unidad)
<div class="three wide field">
<label for="precio{{$unidad->pu_id}}">Precio {{ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion)}} {{$unidad->descripcion}}</label>
<div class="ui right labeled input" id="input{{$unidad->pu_id}}">
<input class="price" type="text" name="precio{{$unidad->pu_id}}" id="precio{{$unidad->pu_id}}" data-id="{{$unidad->pu_id}}" value="{{($unidad->valor > 0) ? $unidad->valor : $unidad->precio($venta->currentEstado()->fecha)->valor}}" />
<div class="ui basic label">UF</div>
</div>
</div>
@endforeach
</div>
<div class="fields">
@foreach($venta->propiedad()->unidades as $unidad)
<div class="three wide field">
@if ($unidad->prorrateo === 0.0)
<label for="prorrateo{{$unidad->id}}">Prorrateo {{ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion)}} {{$unidad->descripcion}}</label>
<div class="ui right labeled input" id="prorrateo{{$unidad->id}}">
<input class="prorrateo" type="text" data-id="{{$unidad->id}}" value="{{$unidad->prorrateo}}" />
<div class="ui basic label">%</div>
</div>
@endif
</div>
@endforeach
</div>
<div class="ui very basic segment" id="total_unidades"></div>
@php $lastDic = new DateTimeImmutable((new DateTimeImmutable())->sub(new DateInterval('P1Y'))->format('31-12-Y')); @endphp
@if (!isset($venta->proyecto()->terreno->fecha) or $venta->proyecto()->terreno->fecha < $lastDic)
<div class="four wide field">
<label for="terreno">Valor Terreno al {{$lastDic->format('d-m-Y')}}</label>
<div class="ui left labeled input">
<div class="ui basic label">$</div>
<input type="number" id="terreno" />
</div>
</div>
@endif
</form>
@if ($venta->currentEstado()->fecha->sub(new DateInterval('P1M')) > $venta->proyecto()->terreno->fecha
and $IPC->get($venta->proyecto()->terreno->fecha, $venta->currentEstado()->fecha->sub(new DateInterval('P1M'))) === 0.0)
<div class="ui compact icon error message">
<i class="exclamation triangle icon"></i>
<div class="content">
IPC no disponible para este mes.
</div>
</div>
@endif
<div class="ui divider"></div>
<div id="factura">
<div class="ui compact grid">
<div class="two columns row">
<div class="twelve wide column">
<strong>
{{mb_strtoupper($venta->proyecto()->inmobiliaria()->nombreCompleto())}}
</strong><br/>
GIRO: <br/>
Dirección: {{$venta->proyecto()->direccion()->simple()}}
</div>
<div class="four wide column">
<div class="ui center aligned red segment">
<strong>
RUT: {{$venta->proyecto()->inmobiliaria()->rut()}}<br/>
FACTURA ELECTRÓNICA<br/>
#
</strong>
</div>
</div>
</div>
<div class="row">
<table class="ui table">
<tr>
<td class="grey"><strong>Señor(es)</strong></td>
<td>{{$venta->propietario()->nombreCompleto()}}</td>
<td class="grey"><strong>RUT</strong></td>
<td>{{$venta->propietario()->rut()}}</td>
</tr>
<tr>
<td class="grey"><strong>Giro</strong></td>
<td>Otras Actividades Profesionales</td>
<td class="grey"><strong>Fecha Emisión</strong></td>
<td>{{(new IntlDateFormatter('es-CL', IntlDateFormatter::LONG, IntlDateFormatter::NONE))->format($venta->currentEstado()->fecha)}}</td>
</tr>
<tr>
<td class="grey"><strong>Dirección</strong></td>
<td>{{$venta->propietario()->datos->direccion->simple()}}</td>
<td class="grey"><strong>Comuna</strong></td>
<td>{{mb_strtoupper($venta->propietario()->datos->direccion->comuna->descripcion)}}</td>
</tr>
</table>
</div>
<div class="row">
<table class="ui celled table">
<thead>
<tr class="grey">
<th class="center aligned" colspan="6">DETALLES</th>
</tr>
<tr class="grey">
<th></th>
<th class="center aligned">Descripción</th>
<th class="center aligned">Cant/Unidad</th>
<th class="center aligned">Prec. Unit.</th>
<th class="center aligned">Ind</th>
<th class="center aligned">Total</th>
</tr>
</thead>
<tbody id="unidades"></tbody>
<tfoot>
<tr>
<td colspan="6">
<br />
<br />
<br />
<br />
</td>
</tr>
</tfoot>
</table>
</div>
<div class="row">
<div class="ten wide column"></div>
<div class="six wide column">
<table class="ui celled very compact table">
<thead>
<tr>
<th class="center aligned grey" colspan="2">TOTALES</th>
</tr>
</thead>
<tbody>
<tr>
<td class="grey">Monto Neto</td>
<td class="right aligned" id="neto"></td>
</tr>
<tr>
<td class="grey">Monto Exento</td>
<td class="right aligned" id="exento"></td>
</tr>
<tr>
<td class="grey">19% IVA</td>
<td class="right aligned" id="iva"></td>
</tr>
<tr>
<td class="grey">Monto Total</td>
<td class="right aligned"><strong id="total"></strong></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<span id="venta"></span>
<div id="facturas"></div>
</div>
@endsection
@include('layout.body.scripts.rut')
@push('page_scripts')
@include('ventas.facturacion.show.factura')
@include('ventas.facturacion.show.propietario')
@include('ventas.facturacion.show.unidad')
@include('ventas.facturacion.show.venta')
<script>
const factura = {
id: '#unidades',
totales: {},
proporcion: 1,
precio: {{$UF->transform($venta->currentEstado()->fecha, $venta->valor)}},
terreno: {{(isset($venta->proyecto()->terreno->fecha) and $venta->proyecto()->terreno->fecha >= $lastDic) ?
$IPC->readjust($venta->proyecto()->terreno->valor, $venta->proyecto()->terreno->fecha, $venta->currentEstado()->fecha) : 0}},
uf: {{$UF->get($venta->currentEstado()->fecha)}},
unidades: JSON.parse('{!! json_encode(array_map(function(Incoviba\Model\Venta\PropiedadUnidad $unidad) use ($venta, $UF, $format) {
$precio = ($unidad->valor > 0) ? $unidad->valor : ($unidad->precio($venta->currentEstado()->fecha) ? $unidad->precio($venta->currentEstado()->fecha)->valor : 0);
return [
'id' => $unidad->id,
'pid' => $unidad->pu_id,
'descripcion' => ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion) . ' ' . $unidad->descripcion,
'precio' => $precio,
'base' => $UF->transform($venta->currentEstado()->fecha, $precio),
'prorrateo' => $unidad->prorrateo,
];
}, $venta->propiedad()->unidades)) !!}'),
build: function() {
const tbody = $(this.id)
tbody.html('')
const pesoFormatter = new Intl.NumberFormat('es-CL', {maximumFractionDigits: 0, minimumFractionDigits: 0})
const ufFormatter = new Intl.NumberFormat('es-CL', {maximumFractionDigits: 2, minimumFractionDigits: 2})
const percentFormatter = new Intl.NumberFormat('es-CL', {maximumFractionDigits: 5, minimumFractionDigits: 5})
let terreno = 0
let prorrateo = 0
let totalUnidades = 0
let precioUnidades = 0
let c = 1
const classes = [
'',
'',
'center aligned',
'right aligned',
'center aligned',
'right aligned'
]
this.unidades.forEach(unidad => {
totalUnidades += unidad.base
precioUnidades += unidad.precio
const descuento = this.terreno * unidad.prorrateo
terreno += descuento
prorrateo += unidad.prorrateo
const bruto = unidad.base - descuento
const neto = bruto / 1.19
const data = [
c ++,
unidad.descripcion + ' (UF ' + ufFormatter.format(unidad.precio * this.proporcion) + ')',
'1 UNID',
pesoFormatter.format(neto * this.proporcion),
'AF',
pesoFormatter.format(neto * this.proporcion)
]
const facturas = {
ids: {},
venta: null,
formatters: {
date: new Intl.DateTimeFormat('es-CL', {year: 'numeric', month: '2-digit', day: '2-digit'}),
pesos: new Intl.NumberFormat('es-CL', {minimumFractionDigits: 0, maximumFractionDigits: 0}),
ufs: new Intl.NumberFormat('es-CL', {minimumFractionDigits: 2, maximumFractionDigits: 2}),
percent: new Intl.NumberFormat('es-CL', {minimumFractionDigits: 4, maximumFractionDigits: 4})
},
draw() {
return {
venta: () => {
document.getElementById(this.ids.venta).innerHTML = this.venta.draw().venta(this.formatters.ufs)
this.venta.watch().venta()
},
facturas: () => {
document.getElementById(this.ids.facturas).innerHTML = this.venta.draw().facturas(this.formatters)
this.venta.watch().facturas()
}
}
},
proporcion() {
if (this.venta.props.facturas.facturas.length === 0) {
return 1
}
return this.venta.props.facturas.facturas.reduce((sum, factura) => {
return sum + factura.props.proporcion
}, 0)
},
setup({ids}) {
this.ids = ids
this.venta = new Venta({
id: {{$venta->id}},
inmobiliaria: {
rut: '{{$venta->proyecto()->inmobiliaria()->rut()}}',
nombre: '{{$venta->proyecto()->inmobiliaria()->nombreCompleto()}}',
direccion: '{{$venta->proyecto()->direccion()->simple()}}',
comuna: '{{$venta->proyecto()->direccion()->comuna->descripcion}}'
},
proyecto: {
id: {{$venta->proyecto()->id}},
},
valor: {{$venta->valor}},
uf: {
fecha: new Date('{{$venta->currentEstado()->fecha->add(new DateInterval('P1D'))->format('Y-m-d')}}'),
valor: {{$uf}}
},
estado: {
fecha: new Date('{{$venta->currentEstado()->fecha->add(new DateInterval('P1D'))->format('Y-m-d')}}')
},
last: {
november: new Date('{{(new DateTimeImmutable())->sub(new DateInterval('P1Y'))->format('Y-11-30')}}')
},
unidades: [
@foreach ($venta->propiedad()->unidades as $unidad)
new Unidad({
id: {{$unidad->id}},
tipo: '{{ucwords($unidad->proyectoTipoUnidad->tipoUnidad->descripcion)}}',
descripcion: '{{ $unidad->descripcion }}',
prorrateo: {{$unidad->prorrateo}},
propiedad_unidad_id: {{$unidad->pu_id}},
valor: {{($unidad->valor > 0) ? $unidad->valor : $unidad->precio($venta->currentEstado()->fecha)->valor}}
}),
@endforeach
],
propietarios: [],
facturas: {
fecha: new Date('{{$venta->currentEstado()->fecha->add(new DateInterval('P1D'))->format('Y-m-d')}}'),
terreno: {
@if ($terreno->fecha != null) fecha: new Date('{{$terreno->fecha->add(new DateInterval('P1D'))->format('Y-m-d')}}'),
@else fecha: null,
@endif
valor: {{($terreno->valor ?? 0) * (1 + $ipc)}}
},
facturas: []
}
})
@if (count($facturas) > 0)
this.venta.props.uf = {
fecha: new Date('{{$facturas[0]->uf->fecha->format('Y-m-d')}}'),
valor: {{$facturas[0]->uf->valor}}
}
@foreach ($facturas as $factura)
this.venta.props.facturas.facturas[{{$factura->index - 1}}] = new Factura({
id: {{ $factura->id }},
venta: this.venta.props,
proporcion: {{ $factura->proporcion }},
index: {{$factura->index}},
fecha: new Date('{{$factura->fecha->format('Y-m-d')}}'),
emisor: {
rut: '{{$factura->venta->proyecto()->inmobiliaria()->rut}}',
nombre: '{{$factura->venta->proyecto()->inmobiliaria()->razon}}',
direccion: '{{$factura->venta->proyecto()->direccion()->simple()}}',
comuna: '{{$factura->venta->proyecto()->direccion()->comuna->id}}',
},
receptor: {
rut: '{{$factura->cliente->rutCompleto()}}',
nombre: '{{$factura->cliente->nombreCompleto()}}',
direccion: '{{$factura->cliente->datos()->direccion->simple()}}',
comuna: '{{$factura->cliente->datos()->direccion->comuna->id}}',
},
terreno: {
@if ($terreno->fecha != null) fecha: new Date('{{$terreno->fecha->add(new DateInterval('P1D'))->format('Y-m-d')}}'),
@else fecha: null,
@endif
valor: {{ $factura->terreno}}
},
unidades: [
@foreach ($factura->unidades as $unidad)
{
unidad: new Unidad({
id: {{$unidad->unidad->id}},
tipo: '{{ucwords($unidad->unidad->proyectoTipoUnidad->tipoUnidad->descripcion)}}',
descripcion: '{{ $unidad->unidad->descripcion }}',
prorrateo: {{$unidad->prorrateo}},
propiedad_unidad_id: {{ array_values(array_filter($venta->propiedad()->unidades, function($uni) use ($unidad) {return $uni->id === $unidad->unidad->id;}))[0]->pu_id }},
valor: {{$unidad->precio}}
}),
descripcion: '{{ucwords($unidad->unidad->proyectoTipoUnidad->tipoUnidad->descripcion)}} {{ $unidad->unidad->descripcion }} (UF {{ number_format(array_values(array_filter($venta->propiedad()->unidades, function($uni) use ($unidad, $factura) {return $uni->id === $unidad->unidad->id;}))[0]->precio($venta->currentEstado()->fecha)->valor * $factura->proporcion, 2) }})',
precio: {{ $unidad->precio }},
prorrateo: {{ $unidad->prorrateo }},
},
@endforeach
],
detalle: {
base: {{ $factura->base() }},
terreno: {{ $factura->terreno * $factura->proporcion * $factura->prorrateo }},
neto: {{ $factura->neto() }},
iva: {{ $factura->iva() }},
bruto: {{ $factura->bruto() }},
total: {{ $factura->total() }},
descuento: {{ array_reduce($factura->unidades, function($sum, $unidad) use ($factura) { return $sum + $unidad->prorrateo * $factura->proporcion; }, 0) }}
},
total: {
neto: {{ array_reduce($factura->unidades, function($sum, $unidad) {return $sum + $unidad->precio * $factura->proporcion;}, 0) }},
exento: {{ $factura->terreno * $factura->proporcion }},
iva: {{ $factura->iva() }},
total: {{ $factura->total() }}
},
saved: true,
uf: {
fecha: new Date('{{$factura->uf->fecha->format('Y-m-d')}}'),
valor: {{$factura->uf->valor}}
}
})
this.venta.props.facturas.facturas[{{$factura->index - 1}}].props.saved = true
this.venta.props.propietarios[{{$factura->index - 1}}] = new Propietario({
index: {{$factura->index}},
proporcion: {{$factura->proporcion}},
rut: '{{$factura->cliente->rutCompleto()}}',
nombre: '{{$factura->cliente->nombreCompleto()}}',
direccion: '{{$factura->cliente->datos()->direccion->simple()}}',
comuna: '{{$factura->cliente->datos()->direccion->comuna->id}}',
fecha: new Date('{{$factura->fecha->format('Y-m-d')}}'),
})
@endforeach
this.draw().venta()
if (this.proporcion() < 1) {
const p = 1 - this.proporcion()
const propietario = this.venta.add().propietario({
rut: '',
nombre: '',
proporcion: (p*100).toFixed(0)/100,
direccion: '',
comuna: '0'
})
document.getElementById('propietarios').innerHTML = this.venta.draw().propietarios()
document.getElementById('cantidad_propietarios').value = this.venta.props.propietarios.length
this.venta.add().factura(propietario)
const row = $('<tr></tr')
data.forEach((value, i) => {
const cell = $('<td></td>')
if (classes[i] !== '') {
cell.addClass(classes[i])
}
cell.html(value)
row.append(cell)
this.draw().facturas()
}
@else
const propietario = this.venta.add().propietario({
proporcion: 1,
rut: '{{$venta->propietario()->rut()}}',
nombre: '{{$venta->propietario()->nombreCompleto()}}',
direccion: '{{$venta->propietario()->datos->direccion->simple()}}',
comuna: '{{$venta->propietario()->datos->direccion->comuna->id}}'
})
tbody.append(row)
})
$('#total_unidades')
.attr('class', 'ui compact segment ' + ((totalUnidades.toFixed(2) !== this.precio.toFixed(2)) ? 'inverted red' : 'inverted green'))
.html('Total Unidades: ' + ufFormatter.format(precioUnidades) + ' UF' +
((totalUnidades.toFixed(2) !== this.precio.toFixed(2)) ? '; Diferencia: ' + ufFormatter.format({{$venta->valor}} - precioUnidades) + ' UF' : ''))
if (totalUnidades.toFixed(2) !== this.precio.toFixed(2)) {
this.highlight()
}
const bruto = this.precio - terreno
const base = bruto / 1.19
const iva = base * .19
const subtotal = base + iva
const total = subtotal + terreno
const totalUF = total / this.uf
const emptyTerreno = '<div class="ui tiny red horizontal circular label">0</div>'
const data = [
c,
'Valor con Terreno ' + pesoFormatter.format((base + terreno) * this.proporcion) + ' - Menos valor terreno ' + ((terreno > 0) ? pesoFormatter.format(-terreno * this.proporcion) : emptyTerreno) + '<br />' +
'Base imponible ' + pesoFormatter.format(base * this.proporcion) + '<br />' +
'IVA ' + pesoFormatter.format(iva * this.proporcion) + '<br />' +
'SUBTOTAL ' + pesoFormatter.format(subtotal * this.proporcion) + '<br />' +
'Mas valor terreno ' + ((terreno > 0) ? pesoFormatter.format(terreno * this.proporcion) : emptyTerreno) + '<br />' +
'TOTAL ' + pesoFormatter.format(total * this.proporcion) + ';' + ufFormatter.format(totalUF * this.proporcion) + ' UF<br /><br />' +
'Descuento Terreno: ' + ((terreno > 0) ? percentFormatter.format(prorrateo * 100) : emptyTerreno) + '%<br /><br />' +
'UF: ' + ufFormatter.format(this.uf),
'1 UNID',
pesoFormatter.format(terreno * this.proporcion),
'EX',
pesoFormatter.format(terreno * this.proporcion)
]
const row = $('<tr></tr>').addClass('top aligned')
data.forEach((value, i) => {
const cell = $('<td></td>')
if (classes[i] !== '') {
cell.addClass(classes[i])
this.draw().venta()
if (typeof propietario !== 'undefined') {
this.venta.add().factura(propietario)
}
cell.html(value)
row.append(cell)
})
tbody.append(row)
$(this.totales.afecto).html(pesoFormatter.format(base * this.proporcion))
$(this.totales.exento).html(pesoFormatter.format(terreno * this.proporcion))
$(this.totales.iva).html(pesoFormatter.format(iva * this.proporcion))
$(this.totales.total).html(pesoFormatter.format(total * this.proporcion))
},
update: function() {
return {
price: (id, value) => {
this.unhighlight()
const idx = this.unidades.findIndex(unidad => unidad.pid === id)
if (idx === -1) {
return
}
const old_value = this.unidades[idx].precio
if (old_value === parseFloat(value)) {
return
}
const url = '{{$urls->api}}/ventas/propiedades/unidad/' + id + '/edit'
const data = new FormData()
data.set('valor', value)
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.edited) {
return
}
const idx = this.unidades.findIndex(unidad => unidad.pid === json.propiedad_unidad_id)
this.unidades[idx].precio = parseFloat(json.input.valor)
this.unidades[idx].base = parseFloat(json.input.valor * this.unidades[idx].base / old_value)
this.build()
})
},
terreno: value => {
const url = '{{$urls->api}}/proyecto/{{$venta->proyecto()->id}}/terreno/edit'
const data = new FormData()
data.set('valor', value)
data.set('fecha', '{{$lastDic->format('Y-m-d')}}')
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.edited) {
return
}
this.terreno = parseInt(json.input.valor)
const data = new FormData()
data.set('start', '{{$lastDic->format('Y-m-d')}}')
data.set('end', '{{$venta->currentEstado()->fecha->sub(new DateInterval('P1M'))->format('Y-m-d')}}')
const url = '{{$urls->api}}/money/ipc'
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
this.terreno *= (1 + parseFloat(json.ipc))
this.build()
})
})
},
prorrateo: (id, value) => {
if (parseFloat(value) === 0) {
return
}
const url = '{{$urls->api}}/ventas/unidad/' + id + '/prorrateo'
const data = new FormData()
data.set('prorrateo', value)
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.edited) {
return
}
const idx = this.unidades.findIndex(unidad => unidad.id === json.unidad_id)
this.unidades[idx].prorrateo = parseFloat(json.input.prorrateo)
this.build()
})
}
}
},
watch: function() {
return {
proporcion: id => {
$(id).change(event => {
const val = $(event.currentTarget).val()
if (val / 100 === this.proporcion) {
return
}
this.proporcion = val / 100
this.build()
})
},
prices: class_name => {
$(class_name).change(event => {
const val = $(event.currentTarget).val()
const id = $(event.currentTarget).data('id')
this.update().price(id, val)
})
},
prorrateo: class_name => {
$(class_name).change(event => {
const val = $(event.currentTarget).val()
const id = $(event.currentTarget).data('id')
this.update().prorrateo(id, val)
})
},
terreno: id => {
$(id).change(event => {
const val = $(event.currentTarget).val()
this.update().terreno(val).then(() => {
$(id).parent().parent().hide()
})
})
}
}
},
highlight: function() {
const pid = this.unidades[0].pid
const input = $('#input' + pid)
input.addClass('error')
input.find('.label').addClass('red')
input.attr('data-content', 'Valor total no es igual a valor de venta')
input.popup()
},
unhighlight: function() {
const pid = this.unidades[0].pid
const input = $('#input' + pid)
input.removeClass('error')
input.find('.label').removeClass('red')
input.removeAttr('data-content')
},
setup: function({form_id, tbody_id, input_id, prices_class, prorrateo_class, terreno_id, totales_ids}) {
$(form_id).submit(event => {
event.preventDefault()
return false
})
this.id = tbody_id
this.totales = totales_ids
this.proporcion = $(input_id).val() / 100
this.watch().proporcion(input_id)
this.watch().prices(prices_class)
this.watch().prorrateo(prorrateo_class)
@if (!isset($venta->proyecto()->terreno->fecha) or $venta->proyecto()->terreno->fecha <= $lastDic)
this.watch().terreno(terreno_id)
@endif
this.build()
this.draw().facturas()
}
}
$(document).ready(() => {
factura.setup({form_id: '#venta_form', tbody_id: '#unidades', input_id: '#proporcion',
prices_class: '.price', prorrateo_class: '.prorrateo', terreno_id: '#terreno', totales_ids: {
afecto: '#neto',
exento: '#exento',
iva: '#iva',
total: '#total'
}})
facturas.setup({
ids: {
venta: 'venta',
facturas: 'facturas'
}
})
})
</script>
@endpush

View File

@ -0,0 +1,431 @@
<script>
class Factura {
props = {
id: 0,
venta: null,
index: 0,
proporcion: 0,
terreno: {
fecha: null,
valor: 0
},
emisor: {
rut: '',
nombre: '',
direccion: '',
comuna: ''
},
receptor: {
rut: '',
nombre: '',
direccion: '',
comuna: ''
},
fecha: null,
unidades: {
unidad: null,
descripcion: '',
precio: 0,
prorrateo: 0
},
detalle: {
base: 0,
terreno: 0,
neto: 0,
iva: 0,
bruto: 0,
descuento: 0,
total: 0
},
total: {
neto: 0,
exento: 0,
iva: 0,
total: 0
},
uf: {
fecha: null,
valor: 0
}
}
constructor(props) {
this.props = props
}
get saved() {
return this.props.id > 0
}
get prorrateo() {
return this.props.unidades.reduce((sum, unidad) => sum + unidad.prorrateo, 0)
}
draw() {
return {
divider: () => {
return '<div class="ui divider" data-index="'+this.props.index+'"></div>'
},
factura: ({formatters = {date, pesos, ufs, percent}}) => {
return [
this.draw().divider(this.props.index),
`<div class="factura" data-index="${this.props.index}">`,
'<div class="ui compact grid">',
this.draw().cabecera(),
this.draw().propietario({formatters}),
this.draw().table({formatters}),
this.draw().totales({formatters}),
this.draw().guardar(),
'</div>',
'</div>'
].join("\n")
},
cabecera: () => {
return [
'<div class="two columns row">',
this.draw().inmobiliaria(),
this.draw().rut(),
'</div>'
].join("\n")
},
inmobiliaria: () => {
return [
'<div class="twelve wide column">',
'<strong>'+this.props.emisor.nombre.toUpperCase()+'</strong><br/>',
'GIRO: <br/>',
`Dirección: ${this.props.emisor.direccion}, ${this.props.emisor.comuna}`,
'</div>',
].join("\n")
},
rut: () => {
return [
'<div class="four wide column">',
'<div class="ui center aligned orange segment">',
'<strong>',
`RUT:${this.props.emisor.rut.toUpperCase()}<br/>`,
'FACTURA ELECTRÓNICA<br/>',
`<span class="ui red text">N° ${this.props.venta.id}${this.props.index}</span>`,
'</strong>',
'</div>',
'</div>'
].join("\n")
},
propietario: ({formatters}) => {
return [
'<div class="row">',
'<table class="ui table">',
'<tr>'+
'<td class="grey"><strong>Señor(es)</strong></td>',
'<td>'+this.props.receptor.nombre+'</td>',
'<td class="grey"><strong>RUT</strong></td>',
'<td>'+this.props.receptor.rut.toUpperCase()+'</td>',
'</tr>',
'<tr>',
'<td class="grey"><strong>Giro</strong></td>',
'<td>Otras Actividades Profesionales</td>',
'<td class="grey"><strong>Fecha Emisión</strong></td>',
'<td>'+formatters.date.format(this.props.fecha)+'</td>',
'</tr>',
'<tr>',
'<td class="grey"><strong>Dirección</strong></td>',
'<td>'+this.props.receptor.direccion+'</td>',
'<td class="grey"><strong>Comuna</strong></td>',
'<td>'+this.props.receptor.comuna.toUpperCase()+'</td>',
'</tr>',
'</table>',
'</div>'
].join("\n")
},
table: ({formatters}) => {
return [
'<div class="row">',
'<table class="ui celled table">',
'<thead>',
'<tr class="grey">',
'<th class="center aligned" colspan="6">DETALLES</th>',
'</tr>',
'<tr class="grey">',
'<th>N°</th>',
'<th class="center aligned">Descripción</th>',
'<th class="center aligned">Cant/Unidad</th>',
'<th class="center aligned">Prec. Unit.</th>',
'<th class="center aligned">Ind</th>',
'<th class="center aligned">Total</th>',
'</tr>',
'</thead>',
'<tbody>',
this.draw().unidades({formatters}),
'</tbody>',
'<tfoot>',
'<tr>',
'<td colspan="6">',
'<br />',
'<br />',
'<br />',
'<br />',
'</td>',
'</tr>',
'</tfoot>',
'</table>',
'</div>'
].join("\n")
},
unidades: ({formatters}) => {
const unidadesData = []
let no = 1
const classes = [
'',
'',
'center aligned',
'right aligned',
'center aligned',
'right aligned'
]
this.props.unidades.forEach(unidad => {
unidadesData.push(this.draw().unidad({
unidad,
no: no++,
classes,
formatters
}))
})
unidadesData.push(this.draw().resumen({
no,
classes,
formatters
}))
return unidadesData.join("\n")
},
unidad: ({unidad, no, classes, formatters}) => {
const descuento = this.props.terreno.valor * unidad.prorrateo * this.props.proporcion
const precio = unidad.precio * this.props.proporcion
const bruto = precio - descuento
const neto = bruto / 1.19
const data = [
no,
unidad.descripcion,
'1 UNID',
formatters.pesos.format(neto),
'AF',
formatters.pesos.format(neto)
]
const row = ['<tr>']
data.forEach((value, i) => {
const cell = ['<td']
if (classes[i] !== '') {
cell.push(' class="' + classes[i] + '"')
}
cell.push('>'+value+'</td>')
row.push(cell.join(''))
})
row.push('</tr>')
return row.join('')
},
resumen: ({no, classes, formatters}) => {
const emptyTerreno = '<div class="ui tiny red horizontal circular label">0</div>'
const data = [
no,
'Valor con Terreno: $' + formatters.pesos.format(this.props.detalle.base) + ' - Menos valor terreno: $' + ((this.props.detalle.terreno > 0) ? formatters.pesos.format(-this.props.detalle.terreno) : emptyTerreno) + '<br />' +
'Base imponible (Neto): $' + formatters.pesos.format(this.props.detalle.neto) + '<br />' +
'IVA: $' + formatters.pesos.format(this.props.detalle.iva) + '<br />' +
'SUBTOTAL (Bruto): $' + formatters.pesos.format(this.props.detalle.bruto) + '<br />' +
'Mas valor terreno: $' + ((this.props.detalle.terreno > 0) ? formatters.pesos.format(this.props.detalle.terreno) : emptyTerreno) + '<br />' +
'TOTAL (Escritura): $' + formatters.pesos.format(this.props.detalle.total) + '; ' + formatters.ufs.format(this.props.venta.valor * this.props.proporcion) + ' UF<br /><br />' +
'Descuento Terreno: ' + ((this.props.detalle.terreno > 0) ? formatters.percent.format(this.props.detalle.descuento * 100) : emptyTerreno) + '%<br /><br />' +
'UF (' + formatters.date.format(this.props.uf.fecha) + '): $' + formatters.ufs.format(this.props.uf.valor),
'1 UNID',
formatters.pesos.format(this.props.detalle.terreno),
'EX',
formatters.pesos.format(this.props.detalle.terreno),
]
const row = ['<tr class="top aligned">']
data.forEach((value, i) => {
const cell = ['<td']
if (classes[i] !== '') {
cell.push(' class="'+classes[i]+'"')
}
cell.push('>'+value+'</td>')
row.push(cell.join(''))
})
return row.join('')
},
totales: ({formatters}) => {
let tooltips = {
neto: null,
iva: null,
total: null
}
if (this.props.total.neto !== this.props.detalle.neto) {
tooltips.neto = ` data-tooltip="No coinciden netos! Promesa: ${formatters.pesos.format(this.props.detalle.neto)} - Unidades: ${formatters.pesos.format(this.props.total.neto)}"`
}
if (this.props.total.iva !== this.props.detalle.iva) {
tooltips.iva = ` data-tooltip="No coinciden IVAs! Promesa: ${formatters.pesos.format(this.props.detalle.iva)} - Unidades: ${formatters.pesos.format(this.props.total.iva)}"`
}
if (this.props.total.total !== this.props.detalle.total) {
tooltips.total = ` data-tooltip="No coinciden totales! Promesa: ${formatters.pesos.format(this.props.detalle.total)} - Unidades: ${formatters.pesos.format(this.props.total.total)}"`
}
return [
'<div class="row">',
'<div class="ten wide column"></div>',
'<div class="six wide column">',
'<table class="ui celled very compact table">',
'<thead>',
'<tr>',
'<th class="center aligned grey" colspan="2">TOTALES</th>',
'</tr>',
'</thead>',
'<tbody>',
'<tr>',
'<td class="grey">Monto Neto</td>',
`<td class="right aligned${this.props.total.neto !== this.props.detalle.neto ? ' red' : ''}"${this.props.total.neto !== this.props.detalle.neto ? tooltips.neto : ''} id="neto">${formatters.pesos.format(this.props.total.neto)}</td>`,
'</tr>',
'<tr>',
'<td class="grey">Monto Exento</td>',
'<td class="right aligned" id="exento">'+formatters.pesos.format(this.props.total.exento)+'</td>',
'</tr>',
'<tr>',
'<td class="grey">19% IVA</td>',
`<td class="right aligned${this.props.total.iva !== this.props.detalle.iva ? ' red' : ''}"${this.props.total.iva !== this.props.detalle.iva ? tooltips.iva : ''} id="iva">${formatters.pesos.format(this.props.total.iva)}</td>`,
'</tr>',
'<tr>',
'<td class="grey">Monto Total</td>',
`<td class="right aligned${(this.props.total.total !== this.props.detalle.total) ? ' red' : ''}"${(this.props.total.total !== this.props.detalle.total) ? tooltips.total : ''}><strong id="total">${formatters.pesos.format(this.props.total.total)}</strong></td>`,
'</tr>',
'</tbody>',
'</table>',
'</div>',
'</div>'
].join("\n")
},
guardar: () => {
if (this.props.saved) {
return [
'<div class="row">',
'<div class="fourteen wide column"></div>',
'<div class="two wide center aligned column">',
`<div class="ui green message guardar" data-index="${this.props.index}">`,
'<i class="check icon"></i>',
'Guardada',
'</div>',
'</div>',
'</div>'
].join("\n")
}
return [
'<div class="row">',
'<div class="right aligned sixteen wide column">',
`<button class="ui primary button guardar" data-index="${this.props.index}">Guardar</button>`,
'</div>',
'</div>'
].join("\n")
}
}
}
watch() {
return {
save: () => {
document.querySelector(`.guardar[data-index="${this.props.index}"]`).addEventListener('click', clickEvent => {
const index = clickEvent.currentTarget.getAttribute('data-index')
facturas.venta.save().factura({index: index - 1})
})
}
}
}
validate() {
if (this.props.venta.id === null || typeof this.props.venta.id === 'undefined') {
return false
}
if (this.props.index === null || typeof this.props.index === 'undefined') {
return false
}
if (this.props.proporcion === null || typeof this.props.proporcion === 'undefined') {
return false
}
return !(this.props.receptor.rut === '' || this.props.receptor.nombre === '' || this.props.receptor.direccion === '' || this.props.receptor.comuna === '');
}
save() {
if (!this.validate()) {
return
}
let url = '{{$urls->api}}/ventas/facturas/add'
if (this.saved) {
url = `{{$urls->api}}/ventas/facturas/${this.props.id}/edit`
}
const method = 'post'
const body = new FormData()
body.set('venta_id', this.props.venta.id)
body.set('index', this.props.index)
body.set('proporcion', this.props.proporcion)
body.set('cliente', JSON.stringify(this.props.receptor))
body.set('terreno', this.props.detalle.terreno)
body.set('unidades', JSON.stringify(this.props.unidades.map(unidad => {
return {unidad_id: unidad.unidad.props.id, precio: unidad.precio, prorrateo: unidad.prorrateo}
})))
body.set('fecha', [this.props.fecha.getFullYear(), this.props.fecha.getMonth()+1, this.props.fecha.getDate()].join('-'))
body.set('detalle', JSON.stringify(this.props.detalle))
body.set('total', JSON.stringify(this.props.total))
body.set('uf', JSON.stringify({fecha: [this.props.uf.fecha.getFullYear(), this.props.uf.fecha.getMonth()+1, this.props.uf.fecha.getDate()].join('-'), valor: this.props.uf.valor}))
return APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (json.success) {
this.props.id = json.factura.id
facturas.draw().facturas()
}
})
})
}
update() {
return {
venta: venta => {
this.props.venta = venta.props
this.props.fecha = venta.props.facturas.fecha
this.props.uf.fecha = venta.props.uf.fecha
this.props.uf.valor = venta.props.uf.valor
this.update().propietario(venta.props.propietarios.find(propietario => propietario.props.index === this.props.index))
this.update().unidades(venta.props.unidades)
this.props.detalle.total = venta.props.valor * this.props.proporcion * venta.props.uf.valor
this.update().detalle(venta.props.facturas.terreno.valor * this.prorrateo)
this.props.detalle.descuento = venta.prorrateo * this.props.proporcion
this.update().total()
},
detalle: terreno => {
this.props.detalle.terreno = terreno * this.props.proporcion
this.props.detalle.bruto = this.props.detalle.total - this.props.detalle.terreno
this.props.detalle.neto = this.props.detalle.bruto / 1.19
this.props.detalle.iva = this.props.detalle.neto * 0.19
this.props.detalle.base = this.props.detalle.neto + this.props.detalle.terreno
},
total: () => {
this.props.total.exento = this.props.detalle.terreno
this.props.total.neto = (this.props.unidades.reduce((sum, unidad) => sum + unidad.precio, 0) * this.props.proporcion - this.props.total.exento) / 1.19
this.props.total.iva = this.props.total.neto * 0.19
this.props.total.total = this.props.total.neto + this.props.total.iva + this.props.total.exento
},
unidades: unidades => {
this.props.unidades = []
unidades.forEach(unidad => {
this.props.unidades.push({
unidad: unidad,
descripcion: unidad.changeDescripcion(this.props.proporcion || 1),
precio: unidad.props.valor * this.props.uf.valor,
prorrateo: unidad.props.prorrateo
})
})
},
propietario: propietario => {
this.props.proporcion = propietario.props.proporcion
this.props.receptor = {
rut: propietario.props.rut ?? '',
nombre: propietario.props.nombre ?? '',
direccion: propietario.props.direccion ?? '',
comuna: propietario.comuna ?? ''
}
}
}
}
}
</script>

View File

@ -0,0 +1,197 @@
<script>
class Propietario {
props = {
index: 0,
proporcion: 0,
rut: '',
nombre: '',
direccion: '',
comuna: ''
}
constructor(props) {
this.props = props
}
get comuna() {
const comuna = $('#comuna_propietario'+this.props.index).dropdown('get text')
if (typeof comuna === 'string') {
return comuna
}
return this.props.comuna ?? ''
}
update() {
return {
proporcion: (valor) => {
this.props.proporcion = valor
facturas.venta.update().facturas()
facturas.draw().facturas()
},
rut: rut => {
rut = rut.replaceAll(/~\d|\.|-/g, '')
const digito = rut.slice(-1)
rut = rut.slice(0, -1)
this.props.rut = Rut.format(rut) + '-' + digito
document.getElementById('rut'+this.props.index).value = this.props.rut
facturas.venta.update().facturas()
facturas.draw().facturas()
},
nombre: nombre => {
this.props.nombre = nombre
facturas.venta.update().facturas()
facturas.draw().facturas()
},
direccion: direccion => {
this.props.direccion = direccion
facturas.venta.update().facturas()
facturas.draw().facturas()
},
comuna: comuna => {
this.props.comuna = comuna
facturas.venta.update().facturas()
facturas.draw().facturas()
},
}
}
watch() {
return {
propietario: () => {
this.watch().proporcion()
this.watch().rut()
this.watch().nombre()
this.watch().direccion()
this.watch().comuna()
},
proporcion: () => {
document.getElementById('proporcion'+this.props.index).addEventListener('input', inputEvent => {
const newValue = inputEvent.currentTarget.value / 100
if (newValue === this.props.proporcion) {
return
}
const saldo = facturas.venta.props.propietarios.reduce((sum, propietario) => sum + propietario.props.proporcion, 0) - this.props.proporcion
if (saldo + newValue > 1) {
return
}
this.update().proporcion(newValue)
})
},
rut: () => {
document.getElementById('rut'+this.props.index).addEventListener('change', inputEvent => {
const rut = inputEvent.currentTarget.value
if (rut === this.props.rut) {
return
}
this.update().rut(rut)
})
},
nombre: () => {
document.getElementById('propietario'+this.props.index).addEventListener('change', inputEvent => {
const nombre = inputEvent.currentTarget.value
if (nombre === this.props.nombre) {
return
}
this.update().nombre(nombre)
})
},
direccion: () => {
document.getElementById('direccion_propietario'+this.props.index).addEventListener('change', inputEvent => {
const direccion = inputEvent.currentTarget.value
if (direccion === this.props.direccion) {
return
}
this.update().direccion(direccion)
})
},
comuna: () => {
if ($('#comuna_propietario'+this.props.index).dropdown('get value')) {
return
}
$('#comuna_propietario'+this.props.index).dropdown({
fireOnInit: true,
forceSelection: true,
onChange: (value, text) => {
this.update().comuna(value)
}
})
},
}
}
draw() {
return {
propietario: () => {
const output = [`<div class="fields" data-index="${this.props.index}">`]
output.push(this.draw().proporcion())
output.push(this.draw().rut())
output.push(this.draw().nombre())
output.push('</div>')
output.push(`<div class="fields" data-index="${this.props.index}">`)
output.push('<div class="three wide field"></div>')
output.push(this.draw().direccion())
output.push(this.draw().comuna())
output.push('</div>')
return output.join("\n")
},
proporcion: () => {
return [
'<div class="two wide field">',
`<label for="proporcion${this.props.index}">Proporción Factura</label>`,
'<div class="ui right labeled input">',
`<input type="number" name="proporcion${this.props.index}" id="proporcion${this.props.index}" value="${this.props.proporcion*100}" max="100" min="0" />`,
'<div class="ui basic icon label">',
'<i class="percent icon"></i>',
'</div>',
'</div>',
'</div>',
].join("\n")
},
rut: () => {
return [
'<div class="three wide field">',
`<label for="rut${this.props.index}">RUT</label>`,
'<div class="ui input">',
`<input type="text" name="rut${this.props.index}" id="rut${this.props.index}" value="${this.props.rut.toUpperCase()}" />`,
'</div>',
'</div>',
].join("\n")
},
nombre: () => {
return [
'<div class="six wide field">',
`<label for="propietario${this.props.index}">Propietario</label>`,
'<div class="ui input">',
`<input type="text" name="propietario${this.props.index}" id="propietario${this.props.index}" value="${this.props.nombre}" />`,
'</div>',
'</div>',
].join("\n")
},
direccion: () => {
return [
'<div class="six wide field">',
`<label for="direccion_propietario${this.props.index}">Dirección</label>`,
'<div class="ui input">',
`<input type="text" name="direccion_propietario${this.props.index}" id="direccion_propietario${this.props.index}" value="${this.props.direccion}" />`,
'</div>',
'</div>',
].join("\n")
},
comuna: () => {
return [
'<div class="three wide field">',
`<label for="comuna_propietario${this.props.index}">Comuna</label>`,
`<div class="ui search selection dropdown" id="comuna_propietario${this.props.index}">`,
`<input type="hidden" name="comuna_propietario${this.props.index}" value="${this.props.comuna}" />`,
'<i class="dropdown icon"></i>',
'<div class="default text">Comuna</div>',
'<div class="menu">',
@foreach ($comunas as $comuna)
'<div class="item" data-value="{{ $comuna->id }}">{{ $comuna->descripcion }}</div>',
@endforeach
'</div>',
'</div>',
'</div>',
].join("\n")
},
}
}
}
</script>

View File

@ -0,0 +1,125 @@
<script>
class Unidad {
props = {
id: 0,
tipo: '',
descripcion: '',
prorrateo: 0,
propiedad_unidad_id: 0,
valor: 0
}
constructor(props) {
this.props = props
}
changeDescripcion(proporcion = 1) {
return this.descripcion = [this.props.tipo, this.props.descripcion, `(UF ${facturas.formatters.ufs.format(this.props.valor * proporcion)})`].join(' ')
}
update() {
return {
precio: newValue => {
const url = '{{$urls->api}}/ventas/propiedades/unidad/' + this.props.propiedad_unidad_id + '/edit'
const method = 'post'
const body = new FormData()
body.set('valor', newValue)
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (!json.edited) {
alert('No se pudo editar la unidad.')
return
}
this.props.valor = parseFloat(json.input.valor)
})
})
},
prorrateo: newValue => {
const url = '{{$urls->api}}/ventas/unidad/' + this.props.id + '/prorrateo'
const method = 'post'
const body = new FormData()
body.set('prorrateo', (parseFloat(newValue) / 100).toFixed(8))
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (!json.edited) {
return
}
this.props.prorrateo = json.input.prorrateo
document.getElementById('prorrateo'+this.props.id).parentElement.parentElement.innerHTML = ''
})
})
}
}
}
watch() {
return {
unidad: () => {
this.watch().precio()
this.watch().prorrateo()
},
precio: () => {
document.getElementById('precio'+this.props.propiedad_unidad_id).addEventListener('change', changeEvent => {
const newValue = changeEvent.currentTarget.value
if (newValue === this.props.valor) {
return
}
this.update().precio(newValue).then(() => {
window.location.reload()
})
})
},
prorrateo: () => {
const input = document.getElementById('prorrateo'+this.props.id)
if (input === null) {
return
}
input.addEventListener('change', changeEvent => {
const newValue = changeEvent.currentTarget.value
if (newValue === this.props.prorrateo) {
return
}
this.update().prorrateo(newValue).then(() => {
facturas.venta.update().totalUnidades()
facturas.draw().facturas()
})
})
}
}
}
draw() {
return {
precio: () => {
return [
'<div class="three wide field">',
'<label for="precio'+this.props.propiedad_unidad_id+'">Precio<br /> '+this.props.tipo+' '+this.props.descripcion+'</label>',
'<div class="ui left labeled input" id="input'+this.props.propiedad_unidad_id+'">',
'<div class="ui basic label">UF</div>',
'<input class="price" type="text" name="precio'+this.props.propiedad_unidad_id+'" id="precio'+this.props.propiedad_unidad_id+'" data-id="'+this.props.propiedad_unidad_id+'" value="'+this.props.valor+'" />',
'</div>',
'</div>'
].join("\n")
},
prorrateo: () => {
const output = []
output.push('<div class="three wide field">')
if (this.props.prorrateo === 0) {
output.push(...[
'<label for="prorrateo'+this.props.id+'">Prorrateo<br /> '+this.props.tipo+' '+this.props.descripcion+'</label>',
'<div class="ui right labeled input">',
'<input class="prorrateo" type="text" id="prorrateo'+this.props.id+'" value="'+this.props.prorrateo+'" />',
'<div class="ui basic label">%</div>',
'</div>'
])
}
output.push('</div>')
return output.join("\n")
}
}
}
}
</script>

View File

@ -0,0 +1,473 @@
<script>
class Venta {
props = {
id: 0,
fecha: null,
last: {
november: null
},
valor: 0,
inmobiliaria: {
rut: '',
nombre: '',
direccion: '',
comuna: ''
},
proyecto: {
id: 0
},
uf: {
fecha: null,
valor: 0
},
ipc: {
fecha: null,
valor: 0
},
unidades: [],
facturas: {
fecha: null,
terreno: {
fecha: null,
valor: 0
},
facturas: []
},
propietarios: [],
estado: {
fecha: null
}
}
constructor(props) {
this.props = props
}
get prorrateo() {
return this.props.unidades.reduce((sum, unidad) => sum + unidad.props.prorrateo, 0.0)
}
update() {
return {
propietarios: count => {
const diff = count - this.props.propietarios.length
if (diff > 0) {
const m = this.props.propietarios.length / count
let p = 1
this.props.propietarios.forEach((propietario, index) => {
this.props.propietarios[index].props.proporcion = parseFloat((this.props.propietarios[index].props.proporcion * m * 100).toFixed(0)) / 100
p -= this.props.propietarios[index].props.proporcion
})
p /= diff
const propietarios = []
for (let i = 0; i < diff; i ++) {
propietarios.push(this.add().propietario({
rut: '',
nombre: '',
proporcion: (p*100).toFixed(0)/100,
direccion: '',
comuna: ''
}))
}
document.getElementById('propietarios').innerHTML = this.draw().propietarios()
propietarios.forEach(propietario => {
this.add().factura(propietario)
})
this.watch().propietarios()
this.update().facturas()
return
}
for (let i = this.props.propietarios.length - 1; i >= count; i --) {
this.remove().propietario(i)
}
},
fecha: date => {
this.props.uf.fecha = date
const url = '{{$urls->api}}/money/uf'
const method = 'post'
const body = new FormData()
body.set('fecha', [date.getFullYear(), date.getMonth()+1, date.getDate()].join('-'))
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (typeof json.uf === 'undefined') {
return
}
this.props.uf.valor = json.uf
return this.update().ipc()
})
})
},
fechaFacturas: date => {
if (this.props.facturas.fecha === date) {
return
}
this.props.facturas.fecha = date
this.update().facturas()
return new Promise(resolve => resolve())
},
totalUnidades: () => {
const unidades = this.props.unidades.reduce((sum, unidad) => sum + unidad.props.valor, 0)
const diff = parseFloat((this.props.valor - unidades).toFixed(4))
const $total = $('#total_unidades')
if (diff === 0) {
$total.html('')
return
}
$total.html('<div class="ui error compact message">' +
'<div class="header">' +
'<i class="exclamation triangle icon"></i>' +
'Diferencia Promesa - Precio Unidades</div>' +
'UF ' + facturas.formatters.ufs.format(diff) +
'</div>')
$total.find('.ui.message').css('display', 'inline-block')
this.update().facturas()
},
terreno: newValue => {
const date = this.props.last.november
const url = '{{$urls->api}}/proyecto/{{$venta->proyecto()->id}}/terreno/edit'
const method = 'post'
const body = new FormData()
body.set('valor', newValue)
body.set('fecha', [date.getFullYear(), date.getMonth()+1, date.getDate()].join('-'))
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (!json.success) {
return
}
this.props.facturas.terreno.fecha = new Date(json.terreno.fecha)
this.props.facturas.terreno.valor = parseInt(json.terreno.valor)
document.getElementById('terreno').parentElement.parentElement.remove()
this.update().ipc()
})
})
},
ipc: () => {
const mesAnterior = new Date([this.props.facturas.fecha.getFullYear(), this.props.facturas.fecha.getMonth()-1, '1'].join('-'))
if (this.props.last.november.getMonth() === mesAnterior.getMonth() && this.props.last.november.getFullYear() === mesAnterior.getFullYear()) {
return
}
const url = '{{$urls->api}}/money/ipc'
const method = 'post'
const body = new FormData()
body.set('start', [this.props.last.november.getFullYear(), this.props.last.november.getMonth()+1, this.props.last.november.getDate()].join('-'))
body.set('end', [mesAnterior.getFullYear(), mesAnterior.getMonth()+1, mesAnterior.getDate()].join('-'))
return fetchAPI(url, {method, body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
this.props.ipc = json.ipc
this.props.facturas.terreno.valor *= (1 + parseFloat(json.ipc.valor))
this.update().facturas()
})
})
},
facturas: () => {
this.props.facturas.facturas.forEach(factura => {
factura.update().venta(this)
})
},
form: () => {
// fecha
// unidades
// valor total unidades
// terreno
// propietarios
// ipc no disponible
}
}
}
watch() {
return {
venta: () => {
this.watch().fecha()
this.watch().fechaFacturas()
this.watch().unidades()
this.watch().terreno()
this.watch().cantidad()
this.watch().propietarios()
this.update().totalUnidades()
},
fecha: () => {
const cdo = structuredClone(calendar_date_options)
cdo['initialDate'] = this.props.estado.fecha
cdo['onChange'] = (date, text, mode) => {
this.update().fecha(date).then(() => {
facturas.draw().facturas()
})
}
$('#fecha_uf').calendar(cdo)
},
fechaFacturas: () => {
const cdo = structuredClone(calendar_date_options)
cdo['initialDate'] = this.props.estado.fecha
cdo['onChange'] = (date, text, mode) => {
this.update().fechaFacturas(date).then(() => {
facturas.draw().facturas()
})
}
$('#fecha_facturas').calendar(cdo)
},
unidades: () => {
this.props.unidades.forEach(unidad => {
unidad.watch().unidad()
})
},
terreno: () => {
const terreno = document.getElementById('terreno')
if (typeof terreno === 'undefined' || terreno === null) {
return
}
terreno.addEventListener('change', changeEvent => {
const newValue = changeEvent.currentTarget.value
if (newValue === this.props.facturas.terreno.valor) {
return
}
this.update().terreno(newValue)
})
},
cantidad: () => {
document.getElementById('cantidad_propietarios').addEventListener('change', changeEvent => {
const count = changeEvent.currentTarget.value
const diff = count - this.props.propietarios.length
if (diff === 0) {
return
}
this.update().propietarios(count)
facturas.draw().facturas()
})
},
propietarios: () => {
this.props.propietarios.forEach(propietario => {
propietario.watch().propietario()
})
},
facturas: () => {
this.props.facturas.facturas.forEach(factura => {
factura.watch().save()
})
}
}
}
draw() {
return {
venta: ufFormatter => {
return [
this.draw().value(ufFormatter),
this.draw().form(),
this.draw().ipc()
].join("\n")
},
value: ufFormatter => {
return [
'<div class="ui grid">',
'<div class="three wide column">',
'<div class="ui very segment">',
'Valor Venta: UF ' + ufFormatter.format(this.props.valor),
'</div>',
'</div>',
'</div>'
].join("\n")
},
form: () => {
const output = []
output.push('<form id="venta_form" class="ui form">')
output.push(this.draw().fecha())
output.push(this.draw().fechaFactura())
output.push(this.draw().precios())
output.push(this.draw().prorrateos())
output.push('<div class="ui very basic segment" id="total_unidades"></div>')
output.push(this.draw().terreno())
output.push(...[
'<div class="two wide field">',
'<label for="propietarios">Propietarios</label>',
'<input type="number" name="cantidad_propietarios" id="cantidad_propietarios" min="1" value="1" />',
'</div>',
])
output.push('<div id="propietarios">')
output.push(this.draw().propietarios())
output.push('</div>')
output.push('</form>')
return output.join("\n")
},
fecha: () => {
return [
'<div class="three wide field">',
'<label for="fecha_uf">Fecha UF</label>',
'<div class="ui calendar" id="fecha_uf">',
'<div class="ui right icon input">',
'<input type="text" name="fecha_uf" />',
'<i class="calendar icon"></i>',
'</div>',
'</div>',
'</div>',
].join("\n")
},
fechaFactura: () => {
return [
'<div class="three wide field">',
'<label for="fecha_uf">Fecha Facturas</label>',
'<div class="ui calendar" id="fecha_facturas">',
'<div class="ui right icon input">',
'<input type="text" name="fecha_facturas" />',
'<i class="calendar icon"></i>',
'</div>',
'</div>',
'</div>',
].join("\n")
},
precios: () => {
const output = []
output.push('<div class="fields">')
this.props.unidades.forEach(unidad => {
output.push(unidad.draw().precio())
})
output.push('</div>')
return output.join("\n")
},
prorrateos: () => {
const output = []
output.push('<div class="fields">')
this.props.unidades.forEach(unidad => {
output.push(unidad.draw().prorrateo())
})
output.push('</div>')
return output.join("\n")
},
terreno: () => {
const output = []
if (typeof this.props.facturas.terreno.fecha === 'undefined' || this.props.facturas.terreno.fecha === null || this.props.facturas.terreno.fecha.getTime() < 0 || this.props.facturas.terreno.fecha < this.props.last.november) {
output.push(...[
'<div class="four wide field">',
'<label for="terreno">Valor Terreno al '+this.props.last.november.toString()+'</label>',
'<div class="ui left labeled input">',
'<div class="ui basic label">$</div>',
'<input type="number" id="terreno" />',
'</div>',
'</div>'
])
}
return output.join("\n")
},
propietarios: () => {
const output = []
this.props.propietarios.forEach(propietario => {
output.push(propietario.draw().propietario())
})
return output.join("\n")
},
ipc: () => {
if (!(this.props.fecha > this.props.facturas.terreno.fecha && this.props.facturas.terreno.valor === 0)) {
return ''
}
return [
'<div class="ui compact icon error message">',
'<i class="exclamation triangle icon"></i>',
'<div class="content">',
'IPC no disponible para este mes.',
'</div>',
'</div>'
].join("\n")
},
facturas: formatters => {
const output = []
this.props.facturas.facturas.forEach(factura => {
output.push(factura.draw().factura({formatters}))
})
return output.join("\n")
}
}
}
add() {
return {
propietario: ({rut, nombre, proporcion, direccion, comuna}) => {
const index = this.props.propietarios.length + 1
const propietario = new Propietario({index, proporcion, rut, nombre, direccion, comuna})
this.props.propietarios.push(propietario)
return propietario
},
factura: propietario => {
const factura = new Factura({
id: 0,
venta: this.props,
index: propietario.props.index,
proporcion: propietario.props.proporcion,
terreno: this.props.facturas.terreno,
emisor: {
rut: this.props.inmobiliaria.rut,
nombre: this.props.inmobiliaria.nombre,
direccion: this.props.inmobiliaria.direccion,
comuna: this.props.inmobiliaria.comuna
},
receptor: {
rut: propietario.props.rut ?? '',
nombre: propietario.props.nombre ?? '',
direccion: propietario.props.direccion ?? '',
comuna: propietario.props.comuna ?? ''
},
unidades: [],
detalle: {
total: this.props.valor * this.props.uf.valor * propietario.props.proporcion,
base: 0,
terreno: 0,
neto: 0,
iva: 0,
bruto: 0,
descuento: 0,
},
total: {
neto: 0,
exento: 0,
iva: 0,
total: this.props.valor * this.props.uf.valor * propietario.props.proporcion
},
uf: this.props.uf
})
factura.update().venta(this)
this.props.facturas.facturas.push(factura)
}
}
}
remove() {
return {
propietario: index => {
if (index <= 0) {
return
}
const P1 = this.props.propietarios.reduce((sum, propietario) => sum + propietario.props.proporcion, 0)
const propietario = this.props.propietarios.splice(index, 1)[0]
const P2 = this.props.propietarios.reduce((sum, propietario) => sum + propietario.props.proporcion, 0)
document.querySelectorAll("[data-index='"+propietario.props.index+"']").forEach(field => {
field.remove()
})
this.remove().factura(index)
},
factura: index => {
this.props.facturas.facturas.splice(index, 1)
document.getElementById('facturas').querySelectorAll("[data-index='"+(index+1)+"']").forEach(factura => {
factura.remove()
})
}
}
}
save() {
return {
factura: ({index}) => {
const factura = this.props.facturas.facturas[index]
return factura.save()
}
}
}
}
</script>

View File

@ -31,7 +31,7 @@
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script type="text/javascript">
<script>
class Venta {
id
propiedad
@ -52,8 +52,12 @@
draw(formatter, dateFormatter) {
const tipo = this.estado.tipo_estado_venta.descripcion
const date = new Date(this.fecha)
let unidad = this.propiedad.unidades[0]
if (this.propiedad.departamentos.length > 0) {
unidad = this.propiedad.departamentos[0]
}
return $('<tr></tr>').append(
$('<td></td>').attr('data-order', this.propiedad.departamentos[0].descripcion).append(
$('<td></td>').attr('data-order', unidad.descripcion).append(
$('<a></a>').attr('href', '{{$urls->base}}/venta/' + this.id).html(this.propiedad.summary)
)
).append(
@ -63,9 +67,9 @@
).append(
$('<td></td>').html(formatter.format(this.valor) + ' UF')
).append(
$('<td></td>').html(this.propiedad.departamentos[0].proyecto_tipo_unidad.abreviacion + ' (' + formatter.format(this.propiedad.departamentos[0].proyecto_tipo_unidad.vendible) + ' m²)')
$('<td></td>').html(unidad.proyecto_tipo_unidad.abreviacion + ' (' + formatter.format(unidad.proyecto_tipo_unidad.vendible) + ' m²)')
).append(
$('<td></td>').html(formatter.format(this.valor / this.propiedad.departamentos[0].proyecto_tipo_unidad.vendible) + ' UF/m²')
$('<td></td>').html(formatter.format(this.valor / unidad.proyecto_tipo_unidad.vendible) + ' UF/m²')
).append(
$('<td></td>').attr('data-order', this.fecha).html(dateFormatter.format(date))
).append(
@ -237,7 +241,12 @@
const tbody = $('<tbody></tbody>')
this.data.ventas.forEach(venta => {
tbody.append(venta.draw(this.formatters.number, this.formatters.date))
try {
tbody.append(venta.draw(this.formatters.number, this.formatters.date))
} catch (error) {
console.debug(venta)
console.error(error)
}
})
table.append(this.draw().header()).append(tbody)
table.show()

View File

@ -20,7 +20,7 @@
@include('layout.body.scripts.datatables')
@push('page_scripts')
<script type="text/javascript">
<script>
const historicos = {
id: '',
data: {

View File

@ -0,0 +1,50 @@
@extends('ventas.base')
@section('venta_subtitle')
Pie
@endsection
@section('venta_content')
<div class="ui basic compact segment">
Valor Promesa: {{ $format->ufs($venta->valor) }} <br />
10% {{ $format->ufs($venta->valor * 0.1) }}
</div>
<form class="ui form" id="add_pie">
<input type="hidden" name="venta" value="{{ $venta->id }}" />
<input type="hidden" name="fecha" value="{{ $venta->fecha->format('Y-m-d') }}" />
<div class="three wide field">
<label for="valor">Valor</label>
<div class="ui right labeled input">
<input type="text" name="valor" id="valor" value="{{ round($venta->valor * 0.1, 2) }}" />
<div class="ui basic label">UF</div>
</div>
</div>
<div class="three wide field">
<label for="cuotas"># Cuotas</label>
<input type="number" name="cuotas" id="cuotas" />
</div>
<button class="ui button">Agregar</button>
</form>
@endsection
@push('page_scripts')
<script>
$(document).ready(() => {
$('#add_pie').submit(event => {
event.preventDefault()
const data = new FormData(event.currentTarget)
return fetchAPI('{{ $urls->api }}/venta/{{ $venta->id }}/pie/add', {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (json.success) {
window.location = '{{$urls->base}}/venta/{{$venta->id}}'
return true
}
return false
})
})
})
</script>
@endpush

View File

@ -0,0 +1,61 @@
@extends('ventas.base')
@section('venta_subtitle')
Agregar Bono - Pie
@endsection
@section('venta_content')
<div class="ui compact segment">
<p>Valor Promesa {{$format->ufs($venta->valor)}}</p>
@if (isset($venta->formaPago()->pie))
<p>Valor Anticipo {{$format->ufs($venta->formaPago()->pie->valor)}}</p>
@endif
<p>Valor 10% {{$format->ufs($venta->valor * 0.1)}}</p>
</div>
<form class="ui form" id="add_bono">
<div class="three wide field">
<label for="fecha">Fecha</label>
<div class="ui calendar" id="fecha">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" placeholder="Fecha" />
</div>
</div>
</div>
<div class="three wide field">
<label for="valor">Valor</label>
<div class="ui right labeled input">
<input type="text" name="valor" id="valor" />
<div class="ui basic label">UF</div>
</div>
</div>
<button class="ui button">Agregar</button>
</form>
@endsection
@push('page_scripts')
<script>
$(document).ready(() => {
calendar_date_options.initialDate = new Date({{$venta->fecha->format('Y, m-1, j')}})
$('#fecha').calendar(calendar_date_options)
$('#add_bono').submit(submitEvent => {
submitEvent.preventDefault()
const url = '{{$urls->api}}/venta/{{$venta->id}}/bono_pie/add'
const data = new FormData()
data.set('fecha', $('#fecha').calendar('get date').toISOString())
data.set('valor', $('#valor').val())
return APIClient.fetch(url, {method: 'post', body: data}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.success) {
return
}
window.location = '{{$urls->base}}/venta/{{$venta->id}}'
})
})
})
</script>
@endpush

View File

@ -0,0 +1,68 @@
@extends('ventas.base')
@section('venta_subtitle')
Editar Bono - Pie
@endsection
@section('venta_content')
<div class="ui compact segment">
<p>Valor Promesa {{$format->ufs($venta->valor)}}</p>
@if (isset($venta->formaPago()->pie))
<p>Valor Anticipo {{$format->ufs($venta->formaPago()->pie->valor)}}</p>
@endif
<p>Valor 10% {{$format->ufs($venta->valor * 0.1)}}</p>
</div>
<form class="ui form" id="add_bono">
<div class="three wide field">
<label for="fecha">Fecha</label>
<div class="ui calendar" id="fecha">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" placeholder="Fecha" />
</div>
</div>
</div>
<div class="three wide field">
<label for="valor">Valor</label>
<div class="ui right labeled input">
<input type="text" name="valor" id="valor" value="{{ round($venta->formaPago()->bonoPie->pago->valor(), 2) }}" />
<div class="ui basic label">UF</div>
</div>
</div>
<button class="ui button">Editar</button>
</form>
@endsection
@include('layout.body.scripts.number_input')
@push('page_scripts')
<script>
$(document).ready(() => {
const fecha = $('#fecha')
fecha.calendar(calendar_date_options)
fecha.calendar('set date', new Date({{$venta->formaPago()->bonoPie->pago->fecha->format('Y, m-1, j')}}))
const numberInput = new NumberInput({input: document.querySelector('input[name="valor"]'), isRational: true})
numberInput.watch()
$('#add_bono').submit(submitEvent => {
submitEvent.preventDefault()
const url = '{{$urls->api}}/venta/{{$venta->id}}/bono_pie/edit'
const method = 'post'
const body = new FormData()
body.set('fecha', $('#fecha').calendar('get date').toISOString())
body.set('valor', numberInput.currentValue)
return APIClient.fetch(url, {method, body}).then(response => {
if (response.ok) {
return response.json()
}
}).then(json => {
if (!json.success) {
return
}
window.location = '{{$urls->base}}/venta/{{$venta->id}}'
})
})
})
</script>
@endpush

View File

@ -4,8 +4,45 @@
Cuotas - Pie
@endsection
@push('page_scripts')
<script>
const bancos = JSON.parse('{!! json_encode($bancos) !!}')
const estados = JSON.parse('{!! json_encode($estados) !!}')
</script>
@endpush
@section('venta_content')
<table class="ui table" id="cuotas">
@if (count($asociadas) > 0)
<div class="ui grid">
<div class="two wide column">Asociados</div>
<div class="six wide column">
{!! implode(' - ', array_map(function(Incoviba\Model\Venta $venta) use ($urls) {
return "<a href=\"{$urls->base}/venta/{$venta->id}\">{$venta->propiedad()->departamentos()[0]->descripcion}</a>";
}, $asociadas)) !!}
</div>
@if ($venta->formaPago()->pie->asociado !== null)
<div class="row">
<div class="ui tiny basic segment">
* Este pie no es la base para el cálculo de las cuotas
</div>
</div>
@endif
</div>
@endif
<div class="ui grid">
<div class="column">Valor</div>
<div class="four wide column">
{{$format->ufs($venta->formaPago()->pie->valor)}}
@if (count($asociadas) > 0)
[{{$format->ufs($venta->formaPago()->pie->valor + array_reduce($asociadas, function(float $sum, Incoviba\Model\Venta $venta) {
return $sum + $venta->formaPago()->pie->valor;
}, 0.0))}}]
@endif
</div>
<div class="column">Cuotas</div>
<div class="column">{{$venta->formaPago()->pie->cuotas}}</div>
</div>
<table class="ui compact table" id="cuotas">
<thead>
<tr>
<th>#</th>
@ -24,45 +61,63 @@
<tbody>@php
$now = new DateTimeImmutable();
$uf_venta = $venta->uf === 0.0 ? $UF->get($venta->currentEstado()->fecha) : $venta->uf;
if ($uf_venta === 0.0) {
$uf_venta = $UF->get();
}
@endphp
@foreach ($venta->formaPago()->pie->cuotas() as $cuota)
<tr data-pago="{{$cuota->pago->id}}"
@if (in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['anulado', 'reemplazado']))
class="disabled"
@endif >
<td>{{$cuota->numero}}</td>
<td>
{{$cuota->numero}}
<button class="ui mini tertiary icon button edit_cuota" data-cuota="{{$cuota->pago->id}}">
<i class="edit icon"></i>
</button>
</td>
<td>{{$cuota->pago->fecha->format('d-m-Y')}}</td>
<td>{{$cuota->pago->fecha->format('Y-m-d')}}</td>
<td>{{$cuota->pago->banco->nombre}}</td>
<td>{{$cuota->pago->identificador}}</td>
<td class="right aligned">{{$format->pesos($cuota->pago->valor)}}</td>
<td class="right aligned">
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'abonado' and $cuota->pago->currentEstado->fecha <= $now)
@if (in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['depositado', 'abonado'])
and $cuota->pago->currentEstado->fecha <= $now)
{{$format->ufs($cuota->pago->valor())}}
@else
~{{$format->ufs($cuota->pago->valor / $uf_venta)}}
@endif
</td>
<td
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'abonado')
class="green"
@elseif ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado')
class="yellow"
@elseif ($cuota->pago->currentEstado->tipoEstadoPago->activo !== 1)
class="red"
@endif
>{{ucwords($cuota->pago->currentEstado->tipoEstadoPago->descripcion)}}</td>
<td{!! ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'abonado' ? ' class="green"' :
($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado' ? ' class="yellow"' :
($cuota->pago->currentEstado->tipoEstadoPago->activo !== 1 ? ' class="red"' : ''))) !!}>
{{ucwords($cuota->pago->currentEstado->tipoEstadoPago->descripcion)}}
<button class="ui mini tertiary icon button edit_estado_cuota" data-cuota="{{$cuota->pago->id}}"
data-estado="{{$cuota->pago->currentEstado->tipoEstadoPago->id}}"
data-fecha="{{$cuota->pago->currentEstado->fecha->format('Y-m-d')}}">
<i class="edit icon"></i>
</button>
</td>
<td>
@if (in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['abonado', 'anulado', 'reemplazado']))
{{$cuota->pago->currentEstado->fecha->format('d-m-Y')}}
@elseif (!in_array($cuota->pago->currentEstado->tipoEstadoPago->descripcion, ['anulado', 'reemplazado']))
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado')
{{$cuota->pago->currentEstado->fecha->format('d-m-Y')}}
@endif
<div class="ui calendar fecha_estado" data-date="{{$cuota->pago->currentEstado->fecha->format('Y-m-d')}}">
<div class="ui action left icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha_estado" />
<button class="ui green basic icon button accept_estado" data-pago="{{$cuota->pago->id}}" data-estado="{{$cuota->pago->currentEstado->tipoEstadoPago->descripcion}}">
<button class="ui green basic icon button accept_estado"
data-pago="{{$cuota->pago->id}}"
data-estado="{{$cuota->pago->currentEstado->tipoEstadoPago->descripcion}}">
<i class="check icon"></i>
</button>
@if ($cuota->pago->currentEstado->tipoEstadoPago->descripcion === 'depositado')
<button class="ui red basic icon button reject_estado" data-pago="{{$cuota->pago->id}}">
<button class="ui red basic icon button reject_estado"
data-pago="{{$cuota->pago->id}}">
<i class="remove icon"></i>
</button>
@endif
@ -85,13 +140,13 @@
<tr>
<th colspan="5">TOTAL</th>
<th class="right aligned">
{{$format->pesos($total_pesos = array_reduce($venta->formaPago()->pie->cuotas(),
function(float $sum, Incoviba\Model\Venta\Cuota $cuota) {
{{$format->pesos($total_pesos = array_reduce($venta->formaPago()->pie->cuotas(vigentes: true),
function(int $sum, Incoviba\Model\Venta\Cuota $cuota) {
return $sum + $cuota->pago->valor;
}, 0))}}
</th>
<th class="right aligned">
{{$format->ufs($total = array_reduce($venta->formaPago()->pie->cuotas(),
{{$format->ufs($total = array_reduce($venta->formaPago()->pie->cuotas(vigentes: true),
function(float $sum, Incoviba\Model\Venta\Cuota $cuota) use ($now, $uf_venta) {
return $sum + (($cuota->pago->fecha > $now or $cuota->pago->uf === null) ?
$cuota->pago->valor / $uf_venta :
@ -103,19 +158,20 @@
<tr>
<th colspan="5">TOTAL PAGADO</th>
<th class="right aligned">
{{$format->pesos($pagado_pesos = array_reduce($venta->formaPago()->pie->cuotas(true),
{{$format->pesos($pagado_pesos = array_reduce($venta->formaPago()->pie->cuotas(pagadas: true, vigentes: true),
function(int $sum, Incoviba\Model\Venta\Cuota $cuota) {
return $sum + $cuota->pago->valor;
}, 0))}}
</th>
<th class="right aligned">
{{$format->ufs($pagado = array_reduce($venta->formaPago()->pie->cuotas(true),
{{$format->ufs($pagado = array_reduce($venta->formaPago()->pie->cuotas(pagadas: true, vigentes: true),
function(float $sum, Incoviba\Model\Venta\Cuota $cuota) {
return $sum + $cuota->pago->valor();
}, 0.0))}}
</th>
<th class="right aligned">
{{$format->number($pagado / $total * 100, 2)}}%
{{ $format->percent(($total > 0) ? $pagado / $total : 0, 2, true) }} Pie<br />
{{ $format->percent($pagado / $venta->valor, 2, true) }} Promesa
</th>
<th colspan="3"></th>
</tr>
@ -128,12 +184,18 @@
{{$format->ufs($total - $pagado)}}
</th>
<th class="right aligned">
{{$format->number(($total - $pagado) / $total * 100, 2)}}%
{{$format->percent(($total > 0) ? ($total - $pagado) / $total * 100 : 0, 2)}}*
</th>
<th colspan="3"></th>
</tr>
</tfoot>
</table>
<div class="ui tiny basic right aligned segment">
* Porcentaje calculado sobre el valor de la venta
</div>
@include('ventas.pies.cuotas.edit')
@include('ventas.pies.cuotas.estados.edit')
@endsection
@include('layout.head.styles.datatables')
@ -142,12 +204,17 @@
@include('layout.body.scripts.datatables.buttons')
@push('page_scripts')
<script type="text/javascript">
<script>
$(document).ready(() => {
function updateRow({pago_id, fecha, color, estado, remove_fecha=false, add_reject=false, disable=false}) {
function updateRow({pago_id, valor, fecha, color, estado, remove_fecha=false, add_reject=false, disable=false}) {
const tr = $("tr[data-pago='" + pago_id + "']")
tr.find(':nth-child(7)').attr('class', color).html(estado)
tr.find(':nth-child(6)').html(valor)
if (typeof color !== 'undefined') {
tr.find(':nth-child(7)').attr('class', color).html(estado)
} else {
tr.find(':nth-child(7)').html(estado)
}
if (remove_fecha) {
tr.find(':nth-child(8)').html(fecha)
}
@ -181,7 +248,7 @@
if (!json.depositado) {
return
}
updateRow({pago_id: json.pago_id, fecha: json.input.fecha, estado: 'Depositado', color: 'yellow', add_reject: true})
updateRow({pago_id: json.pago_id, valor: json.pago.valor_uf, fecha: json.input.fecha, estado: 'Depositado', color: 'yellow', add_reject: true})
button.attr('data-estado', 'depositado')
})
})
@ -189,7 +256,7 @@
function abonar(pago_id, fecha) {
const url = '{{$urls->api}}/ventas/pago/' + pago_id + '/abonar'
const body = new FormData()
body.set('fecha', fecha.toISOString())
body.set('fecha', [fecha.getFullYear(), fecha.getMonth() + 1, fecha.getDate()].join('-'))
return fetchAPI(url, {method: 'post', body}).then(response => {
if (!response) {
return
@ -234,6 +301,66 @@
})
})
}
function editar(id, body) {
const url = '{{$urls->api}}/ventas/pago/' + id
return fetchAPI(url, {method: 'post', body}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (!json.editado) {
return
}
const tr = $(`tr[data-pago='${json.pago.id}']`)
tr.children(':nth-child(1)').html(json.pago.numero)
const fecha = json.pago.fecha.split(' ')[0].split('-').reverse().join('-')
tr.children(':nth-child(2)').html(fecha)
tr.children(':nth-child(3)').html(json.pago.banco.nombre)
tr.children(':nth-child(4)').html(json.pago.identificador)
const pesosFormatter = Intl.NumberFormat('es-CL', {style: 'currency', currency: 'CLP'})
tr.children(':nth-child(5)').html(pesosFormatter.format(json.pago.valor))
const ufFormatter = Intl.NumberFormat('es-CL', {minimumFractionDigits: 2, maximumFractionDigits: 2})
tr.children(':nth-child(6)').html(ufFormatter.format(json.pago.valor_uf) + ' UF')
})
})
}
const editModal = new EditModal({id: '#edit_cuota_modal', approve: ({id, data}) => {
editar(id, data)
}})
$('.edit_cuota').on('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('cuota')
const tr = $(`tr[data-pago='${id}']`)
const number = tr.children(':nth-child(1)').text().split('<')[0].trim()
const fecha = tr.children(':nth-child(2)').text()
const nombre_banco = tr.children(':nth-child(3)').text()
const banco_id = bancos.filter(banco => banco.nombre === nombre_banco)[0].id
const identificador = tr.find(':nth-child(4)').text()
const valor = parseInt(tr.find(':nth-child(5)').text().replace('$', '').replaceAll('.', '').trim())
editModal.open({id, number, fecha, banco_id, identificador, valor})
})
const editCuotaModal = new EditEstadoModal({id: '#edit_estado_cuota_modal', approve: ({id, data}) => {
const url = '{{$urls->api}}/ventas/pago/' + id + '/estado'
return fetchAPI(url, {method: 'post', body: data}).then(response => {
if (!response) {
return
}
return response.json().then(json => {
if (!json.editado) {
return
}
window.location.reload()
})
})
}})
$('.edit_estado_cuota').on('click', clickEvent => {
const id = $(clickEvent.currentTarget).data('cuota')
const estado_id = $(clickEvent.currentTarget).data('estado')
const fecha = $(clickEvent.currentTarget).data('fecha')
editCuotaModal.open({id, estado_id, fecha})
})
$('.fecha_estado').calendar({
type: 'date',
formatter: {
@ -270,17 +397,30 @@
zeroRecords: 'No se encotró cuotas con ese criterio',
search: 'Buscar: '
},
pageLength: "25",
pageLength: "100",
columnDefs: [
{
target: 1,
orderData: [2]
orderData: [2],
className: 'dt-center'
},
{
target: 2,
visible: false,
searchable: false
},
{
target: 3,
className: 'dt-center'
},
{
targets: [5, 6],
className: 'dt-right'
},
{
target: 7,
className: 'dt-center'
},
{
target: 8,
orderData: [9]

View File

@ -86,7 +86,7 @@
@include('layout.body.scripts.dayjs')
@push('page_scripts')
<script type="text/javascript">
<script>
function setDate(index, calendar, date = new Date()) {
const d = dayjs(date)
$(calendar).calendar('set date', new Date(d.add(index, 'M').valueOf()))

View File

@ -0,0 +1,108 @@
<div class="ui mini modal" id="edit_cuota_modal">
<div class="header">Editar Cuota <span class="numero"></span></div>
<div class="content">
<form class="ui form" id="edit_cuota_form">
<input type="hidden" name="id" />
<div class="field">
<label>Fecha de Pago</label>
<div class="ui calendar" id="edit_fecha">
<div class="ui icon input">
<input type="text" name="fecha" />
<i class="calendar icon"></i>
</div>
</div>
</div>
<div class="field">
<label>Banco</label>
<div class="ui search selection dropdown" id="edit_banco">
<i class="dropdown icon"></i>
<input type="hidden" name="banco" />
<div class="default text">Banco</div>
<div class="menu">
@foreach($bancos as $banco)
<div class="item" data-value="{{ $banco->id }}">{{ $banco->nombre }}</div>
@endforeach
</div>
</div>
</div>
<div class="field">
<label>Identificador</label>
<input type="text" name="identificador" />
</div>
<div class="field">
<label>Valor</label>
<div class="ui labeled input">
<div class="ui label">$</div>
<input type="text" name="valor" />
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui positive approve icon button">
<i class="check icon"></i>
</button>
<button class="ui negative cancel icon button">
<i class="times icon"></i>
</button>
</div>
</div>
@push('page_scripts')
<script>
class EditModal {
id
approveCallback
data
constructor({id, approve}) {
this.id = id
this.approveCallback = approve;
$(this.id).modal({
onApprove: $element => {
return this.approve($element)
}
})
$(this.id).find('#edit_fecha').calendar(calendar_date_options)
$(this.id).find('#edit_banco').dropdown()
}
open({id, number, fecha, banco_id, identificador, valor}) {
this.data = {id, fecha: fecha.split('-').reverse().join('-'), banco: banco_id, identificador, valor}
$(this.id).find('.numero').text(number)
$(this.id).find('input[name="id"]').val(id)
const dateParts = fecha.split('-')
const date = new Date(dateParts[2], dateParts[1] - 1, dateParts[0])
$(this.id).find('#edit_fecha').calendar('set date', date)
$(this.id).find('#edit_banco').dropdown('set selected', banco_id)
$(this.id).find('input[name="identificador"]').val(identificador)
$(this.id).find('input[name="valor"]').val(valor)
$(this.id).modal('show')
}
approve($element) {
const $form = $(this.id).find('#edit_cuota_form')
const temp = new FormData(document.getElementById('edit_cuota_form'))
const date = $form.find('#edit_fecha').calendar('get date')
temp.set('fecha', date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0'))
temp.set('banco', $form.find('#edit_banco').dropdown('get value'))
temp.set('valor', temp.get('valor'))
const data = new FormData()
Object.entries(this.data).forEach(([key, value]) => {
if (key === 'id') {
return;
}
if (temp.get(key) === value.toString()) {
return;
}
data.set(key, temp.get(key))
})
return this.approveCallback({id: this.data.id, data})
}
}
</script>
@endpush

View File

@ -0,0 +1,93 @@
<div class="ui mini modal" id="edit_estado_cuota_modal">
<div class="header">Editar Estado Cuota <span class="numero"></span></div>
<div class="content">
<form class="ui form" id="edit_estado_cuota_form">
<input type="hidden" name="id" />
<div class="field">
<label>Estado</label>
<div class="ui search selection dropdown" id="edit_estado_estado">
<i class="dropdown icon"></i>
<input type="hidden" name="estado" />
<div class="default text">Estado</div>
<div class="menu">
@foreach($estados as $estado)
<div class="item" data-value="{{ $estado->id }}">{{ ucwords($estado->descripcion) }}</div>
@endforeach
</div>
</div>
</div>
<div class="field">
<label>Fecha Estado</label>
<div class="ui calendar" id="edit_estado_fecha">
<div class="ui icon input">
<input type="text" name="fecha" />
<i class="calendar icon"></i>
</div>
</div>
</div>
</form>
</div>
<div class="actions">
<button class="ui positive approve icon button">
<i class="check icon"></i>
</button>
<button class="ui negative cancel icon button">
<i class="times icon"></i>
</button>
</div>
</div>
@push('page_scripts')
<script>
class EditEstadoModal {
id
approveCallback
data
constructor({id, approve}) {
this.id = id
this.approveCallback = approve;
$(this.id).modal({
onApprove: $element => {
return this.approve($element)
}
})
$(this.id).find('#edit_estado_estado').dropdown()
$(this.id).find('#edit_estado_fecha').calendar(calendar_date_options)
}
open({id, estado_id, fecha}) {
this.data = {id, fecha: fecha, estado: estado_id}
$(this.id).find('input[name="id"]').val(id)
$(this.id).find('#edit_estado_estado').dropdown('set selected', estado_id)
const dateParts = fecha.split('-')
const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2])
$(this.id).find('#edit_estado_fecha').calendar('set date', date)
$(this.id).modal('show')
}
approve($element) {
const $form = $(this.id).find('#edit_estado_cuota_form')
const temp = new FormData(document.getElementById('edit_estado_cuota_form'))
const date = $form.find('#edit_estado_fecha').calendar('get date')
temp.set('estado', $form.find('#edit_estado_estado').dropdown('get value'))
temp.set('fecha', date.getFullYear() + '-' + (date.getMonth() + 1).toString().padStart(2, '0') + '-' + date.getDate().toString().padStart(2, '0'))
const data = new FormData()
Object.entries(this.data).forEach(([key, value]) => {
if (key === 'id') {
return;
}
if (temp.get(key) === value.toString()) {
return;
}
data.set(key, temp.get(key))
})
return this.approveCallback({id: this.data.id, data})
}
}
</script>
@endpush

View File

@ -24,6 +24,9 @@
<button class="ui tiny green icon button" id="add_button">
<i class="plus icon"></i>
</button>
<button class="ui tiny green icon button" id="import_button">
<i class="upload icon"></i>
</button>
</div>
</div>
</h4>
@ -31,6 +34,7 @@
<table class="ui table" id="list_data"></table>
</div>
</div>
@include('ventas.precios.modal.import')
<div class="ui modal" id="list_modal">
<div class="header">
Actualizar <span id="modal_title"></span>
@ -73,7 +77,7 @@
@endpush
@push('page_scripts')
<script type="text/javascript">
<script>
class Unidad {
id
nombre
@ -97,7 +101,8 @@
return this.precio / this.superficie
}
draw(formatter) {
const date = new Date(this.fecha)
const dateParts = this.fecha.split('-')
const date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2])
const dateFormatter = new Intl.DateTimeFormat('es-CL')
return $('<tr></tr>').addClass('unidad').attr('data-linea', this.linea).append(
$('<td></td>').html(this.nombre)
@ -317,7 +322,8 @@
buttons: {
add: '',
up: '',
refresh: ''
refresh: '',
import: ''
}
},
data: {
@ -330,6 +336,11 @@
loading: {
precios: false
},
components: {
modals: {
import: null
}
},
get: function() {
return {
proyectos: () => {
@ -378,6 +389,24 @@
}
}
},
url() {
return {
proyectos: () => {
const currentUrl = window.location.href
const newUrl = `{{ $urls->base }}/ventas/precios`
if (newUrl !== currentUrl) {
window.history.replaceState(null, null, newUrl)
}
},
precios: proyecto_id => {
const currentUrl = window.location.href
const newUrl = `{{ $urls->base }}/ventas/precios/${proyecto_id}`
if (newUrl !== currentUrl) {
window.history.replaceState(null, null, newUrl)
}
}
}
},
add: function() {
return {
precio: data => {
@ -400,6 +429,7 @@
draw: function() {
return {
proyectos: () => {
this.url().proyectos()
const parent = $(this.ids.list)
const header = parent.find('#list_title')
const list = parent.find('.list')
@ -408,6 +438,7 @@
$(this.ids.buttons.add).hide()
$(this.ids.buttons.add).attr('data-id', '')
$(this.ids.buttons.add).attr('data-proyecto', '')
$(`#${this.ids.buttons.import}`).hide()
header.html('Proyectos')
table.hide()
@ -415,7 +446,8 @@
this.data.proyectos.forEach(proyecto => {
list.append(
$('<div></div>').addClass('item proyecto').attr('data-proyecto', proyecto.id).html(proyecto.descripcion).css('cursor', 'pointer')
$('<div></div>').addClass('item proyecto').attr('data-proyecto', proyecto.id)
.html(proyecto.descripcion).css('cursor', 'pointer')
)
})
list.show()
@ -431,6 +463,7 @@
})
},
precios: () => {
this.url().precios(this.data.id)
const parent = $(this.ids.list)
const header = parent.find('#list_title')
const list = parent.find('.list')
@ -440,6 +473,8 @@
$(this.ids.buttons.add).attr('data-proyecto', this.data.proyecto)
$(this.ids.buttons.add).show()
$(`#${this.ids.buttons.import}`).show()
header.html('Precios de ' + this.data.proyecto)
list.hide()
table.html('')
@ -581,18 +616,27 @@
}
}
},
import: event => {
event.preventDefault()
precios.components.modals.import.show(this.data.id)
return false
}
}
},
setup: function({list, proyectos, buttons_up, buttons_refresh, buttons_add}) {
setup: function({list, proyectos, buttons_up, buttons_refresh, buttons_add, buttons_import}) {
this.ids.list = list
this.ids.proyectos = proyectos
this.ids.buttons.up = buttons_up
this.ids.buttons.refresh = buttons_refresh
this.ids.buttons.add = buttons_add
this.ids.buttons.import = buttons_import
$(this.ids.buttons.up).click(this.actions().up)
$(this.ids.buttons.refresh).click(this.actions().refresh)
$(this.ids.buttons.add).click(this.actions().add().list)
document.getElementById(this.ids.buttons.import).addEventListener('click', this.actions().import)
this.components.modals.import = new ImportModal()
this.draw().proyectos()
}
@ -674,6 +718,10 @@
$(this.ids.button).click(this.actions().send)
}
}
function selectProject(projectId) {
const $project = $(`.item.proyecto[data-proyecto="${projectId}"]`)
$project.click()
}
$(document).ready(() => {
precios.setup({
@ -681,7 +729,8 @@
proyectos: '#proyectos',
buttons_up: '#up_button',
buttons_refresh: '#refresh_button',
buttons_add: '#add_button'
buttons_add: '#add_button',
buttons_import: 'import_button'
})
list_modal.setup({
modal: '#list_modal',
@ -692,6 +741,10 @@
fields_valor: '#valor',
button: '#send'
})
@if (isset($project_id))
selectProject({{$project_id}})
@endif
})
</script>
@endpush

View File

@ -0,0 +1,120 @@
<div class="ui modal" id="import_modal">
<div class="header">
Importar Precios
</div>
<div class="content">
<div class="ui form">
<input type="hidden" id="import_project_id" name="import_project_id" value="" />
<div class="three wide field">
<div class="ui calendar" id="import_date">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="fecha" />
</div>
</div>
</div>
<input class="ui invisible file input" type="file" id="import_file" name="file" />
<label class="ui placeholder segment" for="import_file">
<div class="ui icon header">
<i class="upload icon"></i>
Archivo de Precios
</div>
</label>
</div>
</div>
<div class="actions">
<div class="ui red cancel icon button">
<i class="remove icon"></i>
</div>
<div class="ui green ok icon button">
<i class="checkmark icon"></i>
Importar
</div>
</div>
@push('page_scripts')
<script>
class ImportModal {
ids = {
modal: '',
project: '',
calendar: '',
file: ''
}
components = {
$modal: null,
form: null,
project: null,
$calendar: null,
file: null,
$file: null
}
constructor() {
this.ids.modal = 'import_modal'
this.ids.project = 'import_project_id'
this.ids.calendar = 'import_date'
this.ids.file = 'import_file'
this.setup()
}
show(project_id) {
this.components.project.value = project_id
this.components.$modal.modal('show')
}
dragDrop(event) {
event.preventDefault()
if (event.originalEvent.dataTransfer && event.originalEvent.dataTransfer.files.length > 0) {
this.components.file.files = event.originalEvent.dataTransfer.files
}
}
import() {
const url = '{{ $urls->api }}/ventas/precios/import'
const method = 'post'
const body = new FormData()
body.set('project_id', this.components.project.value)
const date = this.components.$calendar.calendar('get date')
body.set('date', [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'))
body.set('file', this.components.file.files[0])
APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (json.status === true) {
window.location.reload()
return
}
console.debug(json)
})
}
setup() {
this.components.$modal = $(`#${this.ids.modal}`)
this.components.$modal.modal({
onApprove: () => {
this.import()
}
})
this.components.form = this.components.$modal.find('form')
this.components.form.submit(event => {
event.preventDefault()
this.import()
return false
})
this.components.project = document.getElementById(this.ids.project)
this.components.$calendar = $(`#${this.ids.calendar}`)
const cdo = structuredClone(calendar_date_options)
cdo['maxDate'] = new Date()
this.components.$calendar.calendar(cdo)
this.components.file = document.getElementById(this.ids.file)
this.components.$file = $(this.components.file.parentNode.querySelector('label'))
this.components.$file.css('cursor', 'pointer')
this.components.$file.on('dragover', event => {
event.preventDefault()
event.stopPropagation()
})
this.components.$file.on('dragenter', event => {
event.preventDefault()
event.stopPropagation()
})
this.components.$file.on('drop', this.dragDrop.bind(this))
}
}
</script>
@endpush

View File

@ -0,0 +1,176 @@
@extends('ventas.promotions.base')
@section('promotions_content')
<table class="ui table" id="promotions">
<thead>
<tr>
<th>Promoción</th>
<th>Tipo</th>
<th>Valor</th>
<th>Fecha Inicio</th>
<th>Fecha Término</th>
<th>Válido Hasta</th>
<th>Contratos</th>
<th class="right aligned">
<button type="button" class="ui small tertiary green icon button" id="add_button">
<i class="plus icon"></i>
</button>
</th>
</tr>
</thead>
<tbody>
@foreach($promotions as $promotion)
<tr>
<td>
<a href="{{ $urls->base }}/ventas/promotion/{{ $promotion->id }}">
{{ $promotion->description }}
<i class="angle right icon"></i>
</a>
</td>
<td>{{ ucwords($promotion->type->name()) }}</td>
<td>{{ ($promotion->type === Incoviba\Model\Venta\Promotion\Type::FIXED) ? $format->ufs($promotion->amount) : $format->percent($promotion->amount, 2, true) }}</td>
<td>{{ $promotion->startDate->format('d-m-Y') }}</td>
<td>{{ $promotion->endDate?->format('d-m-Y') }}</td>
<td>{{ $promotion->validUntil?->format('d-m-Y') }}</td>
<td>
Proyectos: {{ count($promotion->projects()) +
count(array_unique(array_map(function($unitType) {return $unitType->project;},$promotion->unitTypes()))) +
count(array_unique(array_map(function($unitLine) {return $unitLine->proyecto;},$promotion->unitLines()))) +
count($promotion->units()) }} <br />
Operadores: {{ count($promotion->brokers()) }}
</td>
<td class="right aligned">
<button type="button" class="ui small tertiary icon button edit_button" data-id="{{ $promotion->id }}">
<i class="edit icon"></i>
</button>
<button type="button" class="ui red small tertiary icon button remove_button" data-id="{{ $promotion->id }}">
<i class="trash icon"></i>
</button>
</td>
</tr>
@endforeach
</tbody>
</table>
@include('ventas.promotions.add_modal')
@include('ventas.promotions.edit_modal')
@endsection
@push('page_scripts')
<script>
const promotions = {
ids: {
buttons: {
add: '',
edit: '',
remove: ''
},
},
handlers: {
add: null,
edit: null,
},
data: JSON.parse('{!! json_encode($promotions) !!}'),
execute() {
return {
add: data => {
const url = '{{$urls->api}}/ventas/promotions/add'
const method = 'post'
const body = new FormData()
body.set('promotions[]', JSON.stringify(data))
return APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
console.error(response.errors)
alert('No se pudo agregar promoción.')
return
}
return response.json().then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo agregar promoción.')
return
}
window.location.reload()
})
})
},
edit: data => {
const url = '{{$urls->api}}/ventas/promotions/edit'
const method = 'post'
const body = new FormData()
body.set('promotions[]', JSON.stringify(data))
return APIClient.fetch(url, {method, body}).then(response => {
if (!response) {
console.error(response.errors)
alert('No se pudo editar promoción.')
return
}
return response.json().then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo editar promoción.')
return
}
window.location.reload()
})
})
},
remove: promotion_id => {
const url = `{{$urls->api}}/ventas/promotion/${promotion_id}/remove`
const method = 'delete'
return APIClient.fetch(url, {method}).then(response => {
if (!response) {
console.error(response.errors)
alert('No se pudo eliminar promoción.')
return
}
return response.json().then(json => {
if (!json.success) {
console.error(json.errors)
alert('No se pudo eliminar promoción.')
return
}
window.location.reload()
})
})
}
}
},
watch() {
document.getElementById(promotions.ids.buttons.add).addEventListener('click', clickEvent => {
clickEvent.preventDefault()
promotions.handlers.add.show()
})
Array.from(document.getElementsByClassName(promotions.ids.buttons.edit)).forEach(button => {
button.addEventListener('click', clickEvent => {
const id = clickEvent.currentTarget.dataset.id
promotions.handlers.edit.load(id)
})
})
Array.from(document.getElementsByClassName(promotions.ids.buttons.remove)).forEach(button => {
button.addEventListener('click', clickEvent => {
clickEvent.preventDefault()
const id = clickEvent.currentTarget.dataset.id
promotions.execute().remove(id)
})
})
},
setup(ids) {
promotions.ids = ids
promotions.handlers.add = new AddModal()
promotions.handlers.edit = new EditModal(promotions.data)
promotions.watch()
}
}
$(document).ready(() => {
promotions.setup({
buttons: {
add: 'add_button',
edit: 'edit_button',
remove: 'remove_button'
}
})
})
</script>
@endpush

View File

@ -0,0 +1,100 @@
<div class="ui modal" id="add_promotion_modal">
<div class="header">
Agregar Promoción
</div>
<div class="content">
<form class="ui form" id="add_promotion_form">
<div class="field">
<label for="description">Descripción</label>
<input type="text" id="description" name="description" placeholder="Descripción" required />
</div>
<div class="fields">
<div class="field">
<label for="type">Tipo</label>
<select id="type" name="type" class="ui select dropdown" required>
<option value="1">Valor Fijo</option>
<option value="2">Valor Variable</option>
</select>
</div>
<div class="field">
<label for="value">Valor</label>
<input type="text" id="value" name="value" placeholder="Valor" required />
</div>
</div>
<div class="fields">
<div class="field">
<label for="start_date">Fecha de Inicio</label>
<div class="ui calendar" id="start_date">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="start_date" placeholder="Fecha de Inicio" required />
</div>
</div>
</div>
<div class="field">
<label for="end_date">Fecha de Término</label>
<div class="ui calendar" id="end_date">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="end_date" placeholder="Fecha de Término" />
</div>
</div>
</div>
<div class="field">
<label for="valid_range">Válido por N Días Después del Término</label>
<input type="text" id="valid_range" name="valid_range" placeholder="Válido por N Días" value="7" required />
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Agregar
<i class="checkmark icon"></i>
</div>
</div>
@push('page_scripts')
<script>
class AddModal {
ids = {
modal: '#add_promotion_modal',
}
modal
constructor() {
this.modal = $(this.ids.modal)
this.modal.find('.ui.dropdown').dropdown()
this.modal.find('.ui.calendar').calendar(calendar_date_options)
this.modal.modal({
onApprove: () => {
const form = document.getElementById('add_promotion_form')
const type = $(form.querySelector('[name="type"]')).dropdown('get value')
const start_date = $(form.querySelector('#start_date')).calendar('get date')
const end_date = $(form.querySelector('#end_date')).calendar('get date')
let valid_until = null
if (end_date !== null) {
valid_until = new Date(end_date)
valid_until.setDate(valid_until.getDate() + parseInt(form.querySelector('[name="valid_range"]').value))
}
const data = {
description: form.querySelector('[name="description"]').value,
type,
value: form.querySelector('[name="value"]').value,
start_date: [start_date.getFullYear(), start_date.getMonth() + 1, start_date.getDate()].join('-'),
end_date: end_date === null ? null : [end_date.getFullYear(), end_date.getMonth() + 1, end_date.getDate()].join('-'),
valid_until: valid_until === null ? null : [valid_until.getFullYear(), valid_until.getMonth() + 1, valid_until.getDate()].join('-'),
}
promotions.execute().add(data)
}
})
}
show() {
this.modal.modal('show')
}
}
</script>
@endpush

View File

@ -0,0 +1,22 @@
@extends('layout.base')
@section('page_title')
@hasSection('promotions_title')
Promoción - @yield('promotions_title')
@else
Promociones
@endif
@endsection
@section('page_content')
<div class="ui container">
<h2 class="ui header">
@hasSection('promotions_header')
Promoción - @yield('promotions_header')
@else
Promociones
@endif
</h2>
@yield('promotions_content')
</div>
@endsection

View File

@ -0,0 +1,126 @@
<div class="ui modal" id="edit_promotion_modal">
<div class="header">
Editar Promoción - <span class="description"></span>
</div>
<div class="content">
<form class="ui form" id="edit_promotion_form">
<input type="hidden" name="id" />
<div class="field">
<label for="description">Descripción</label>
<input type="text" id="description" name="description" placeholder="Descripción" required />
</div>
<div class="fields">
<div class="field">
<label for="type">Tipo</label>
<select id="type" name="type" class="ui select dropdown" required>
<option value="1">Valor Fijo</option>
<option value="2">Valor Variable</option>
</select>
</div>
<div class="field">
<label for="value">Valor</label>
<input type="text" id="value" name="value" placeholder="Valor" required />
</div>
</div>
<div class="fields">
<div class="field">
<label for="edit_start_date">Fecha de Inicio</label>
<div class="ui calendar" id="edit_start_date">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="edit_start_date" placeholder="Fecha de Inicio" required />
</div>
</div>
</div>
<div class="field">
<label for="edit_end_date">Fecha de Término</label>
<div class="ui calendar" id="edit_end_date">
<div class="ui left icon input">
<i class="calendar icon"></i>
<input type="text" name="edit_end_date" placeholder="Fecha de Término" />
</div>
</div>
</div>
<div class="field">
<label for="valid_range">Válido por N Días Después del Término</label>
<input type="text" id="valid_range" name="valid_range" placeholder="Válido por N Días" value="7" required />
</div>
</div>
</form>
</div>
<div class="actions">
<div class="ui black deny button">
Cancelar
</div>
<div class="ui positive right labeled icon button">
Editar
<i class="checkmark icon"></i>
</div>
</div>
@push('page_scripts')
<script>
class EditModal {
ids = {
modal: '#edit_promotion_modal',
}
modal
promotions
constructor(data) {
this.promotions = data
this.modal = $(this.ids.modal)
this.modal.find('.ui.dropdown').dropdown()
this.modal.find('.ui.calendar').calendar(calendar_date_options)
this.modal.modal({
onApprove: () => {
const form = document.getElementById('edit_promotion_form')
const type = $(form.querySelector('[name="type"]')).dropdown('get value')
const start_date = $(form.querySelector('#edit_start_date')).calendar('get date')
const end_date = $(form.querySelector('#edit_end_date')).calendar('get date')
let valid_until = null
if (end_date !== null) {
valid_until = new Date(end_date)
valid_until.setDate(valid_until.getDate() + parseInt(form.querySelector('[name="valid_range"]').value))
}
const data = {
id: form.querySelector('[name="id"]').value,
description: form.querySelector('[name="description"]').value,
type,
value: form.querySelector('[name="value"]').value,
start_date: [start_date.getFullYear(), start_date.getMonth() + 1, start_date.getDate()].join('-'),
end_date: end_date === null ? null : [end_date.getFullYear(), end_date.getMonth() + 1, end_date.getDate()].join('-'),
valid_until: valid_until === null ? null : [valid_until.getFullYear(), valid_until.getMonth() + 1, valid_until.getDate()].join('-'),
}
promotions.execute().edit(data)
}
})
}
load(promotion_id) {
const promotion = this.promotions.find(promotion => promotion.id === parseInt(promotion_id))
const form = document.getElementById('edit_promotion_form')
form.reset()
form.querySelector('[name="id"]').value = promotion.id
form.querySelector('[name="description"]').value = promotion.description
form.querySelector('[name="type"]').value = promotion.type
form.querySelector('[name="value"]').value = promotion.amount
const start_date_parts = promotion.start_date.split('-')
const start_date = new Date(start_date_parts[0], start_date_parts[1] - 1, start_date_parts[2])
$('#edit_start_date').calendar('set date', start_date)
if (promotion.end_date !== null) {
const end_date_parts = promotion.end_date.split('-')
const end_date = new Date(end_date_parts[0], end_date_parts[1] - 1, end_date_parts[2])
$('#edit_end_date').calendar('set date', end_date)
if (promotion.valid_until !== null) {
const valid_until_parts = promotion.valid_until.split('-')
const valid_until = new Date(valid_until_parts[0], valid_until_parts[1] - 1, valid_until_parts[2])
form.querySelector('[name="valid_range"]').value = valid_until.getDate() - end_date.getDate()
}
}
this.modal.modal('show')
}
}
</script>
@endpush

Some files were not shown because too many files have changed in this diff Show More