Compare commits

58 Commits

Author SHA1 Message Date
fd357ca1c1 FIX: form was being sent and also "submitted". Prefilled values gave errors when not set. 2025-12-03 19:15:17 -03:00
d89c1b5815 FIX: mapping of prices import
New template with filled units
2025-11-26 17:10:55 -03:00
423f8ddf4c Reservation to Venta add form 2025-11-26 16:23:32 -03:00
9ed918ee03 Reservation to Venta add form 2025-11-25 17:59:21 -03:00
5f348e954e FIX: edit payments 2025-11-25 13:22:52 -03:00
c9056451eb Move to add date 2025-11-25 11:45:52 -03:00
07a1607554 Set default date 2025-11-24 19:07:56 -03:00
a0019b51a5 Skip date if none provided 2025-11-24 19:02:29 -03:00
49f5aa21f9 Check for lower case titles 2025-11-24 18:55:51 -03:00
111c7df6b4 Better look 2025-11-24 18:48:17 -03:00
2b120a3bf5 Add price template file 2025-11-24 18:40:52 -03:00
caf0dfe4ab Edit reservation 2025-11-13 19:15:20 -03:00
a6037e4e14 Edit modal, validation of whole data 2025-11-13 18:57:58 -03:00
539ac3c0e8 Changed to _payment 2025-11-13 15:20:27 -03:00
c3a7e7636f Fill payment when editing 2025-11-13 15:19:52 -03:00
a41f306d3f Reservation Payment detail 2025-11-13 15:19:33 -03:00
81ac48afbf Missing exception history 2025-11-13 14:55:28 -03:00
54969eeeab Filling edit form 2025-11-12 19:49:08 -03:00
e278695137 Load api data once 2025-11-12 17:31:55 -03:00
c99690e7b6 Merge branch 'develop' into feature/cierres 2025-11-12 11:56:28 -03:00
bc5967b2be FIX: Proveedor::contacto == null 2025-11-12 10:28:01 -03:00
4760b08673 Common components 2025-11-12 10:16:19 -03:00
c254ac11fe Merge branch 'develop' into feature/cierres 2025-11-10 18:42:44 -03:00
f440d771db Formatting 2025-11-10 18:42:31 -03:00
270a07bb77 Edit reservations 2025-11-09 17:50:46 -03:00
f8ea44460d FIX: Cartolas with empty values 2025-10-27 21:27:32 -03:00
59115bd631 Merge branch 'feature/cierres' into develop 2025-10-25 12:51:27 -03:00
2edcdacbe0 FIX: promotions id 2025-10-25 12:49:58 -03:00
ebaf708f79 Added calculation detail 2025-10-25 12:05:32 -03:00
14d75b1ce9 Findic IPC 2025-10-25 11:23:00 -03:00
ab60c9db1b FIX: wrong negation 2025-10-24 19:32:33 -03:00
04a725e517 Added new Money provider 2025-10-24 18:35:52 -03:00
6e32b1debc Merge branch 'feature/cierres' into develop 2025-10-17 19:47:14 -03:00
5e89e9c830 FIX: Missing promotion data for reservation 2025-10-17 19:45:54 -03:00
ecbff7bfcd Merge pull request 'feature/cierres' (#50) from feature/cierres into develop
Reviewed-on: http://git.provm.cl/Incoviba/oficial/pulls/50
2025-10-17 19:32:39 -03:00
e4100e7f21 Show table tooltips 2025-10-17 19:25:54 -03:00
b75c4b7efc FIX: Format date 2025-10-17 19:25:25 -03:00
9794070925 FIX: Skip blank values 2025-10-17 19:25:12 -03:00
1ca087e19b FIX: Show global promotions correctly 2025-10-17 18:01:49 -03:00
4bd3c7af9b FIX: remove icon 2025-10-17 13:48:07 -03:00
815d81c9c3 Forma de pago en reserva 2025-10-17 13:46:03 -03:00
434c1c6d06 FIX: infinite loop 2025-10-17 12:48:14 -03:00
b986e63cc0 FIX: address exists 2025-10-17 12:38:57 -03:00
74c6347367 Merge branch 'develop' into feature/cierres 2025-10-17 12:28:27 -03:00
5ea300837e FIX: exceptions list 2025-10-17 12:27:43 -03:00
71309e13a0 Correct exceptions 2025-10-17 12:26:46 -03:00
beb73ba6df Comparacion con valor base 2025-10-17 11:59:34 -03:00
06558d778d Merge pull request 'FIX: Skip handling import errors' (#49) from fix/add-venta into develop
Reviewed-on: http://git.provm.cl/Incoviba/oficial/pulls/49
2025-10-06 12:08:39 -03:00
538833a5a4 FIX: Skip handling import errors 2025-10-06 12:08:08 -03:00
7bee4e3bb4 Merge pull request 'FIX: Cartolas not adding Movimientos or adding cargos and abonos' (#46) from fix/carga-cartolas into develop
Reviewed-on: http://git.provm.cl/Incoviba/oficial/pulls/46
2025-10-04 14:16:29 -03:00
0c640d6cab FIX: Cartolas not adding Movimientos or adding cargos and abonos 2025-10-04 14:14:13 -03:00
6d871d77fe Merge branch 'master' into develop 2025-10-04 11:39:53 -03:00
b0267320a1 fix/add-venta (#44)
FIXES:
 - cast Comuna.id to int in Propietario
 - Inmobiliaria without tipoSociedad not loading descripcion

Log exception processor
Get Money values when stored as 0

Co-authored-by: Juan Pablo Vial <jpvialb@incoviba.cl>
Reviewed-on: http://git.provm.cl/Incoviba/oficial/pulls/44
2025-10-03 12:13:02 -03:00
6ddc48ec60 Merge branch 'develop' 2024-03-13 16:31:34 -03:00
331ee1e584 Merge branch 'develop' 2023-06-22 23:18:13 -04:00
24c17debf3 Merge branch 'develop' 2023-02-15 18:30:09 -03:00
552fd0aa06 Merge branch 'develop' 2023-02-14 20:50:42 -03:00
60faf293d4 Merge branch 'develop' 2023-02-13 17:18:41 -03:00
60 changed files with 3401 additions and 730 deletions

View File

@ -2,6 +2,7 @@
namespace Incoviba\Common\Define\Cartola;
use Psr\Http\Message\UploadedFileInterface;
use Incoviba\Exception\ServiceAction\Read;
interface Banco
{
@ -9,6 +10,7 @@ interface Banco
* Process bank movements for database inserts
* @param UploadedFileInterface $file
* @return array
* @throws Read
*/
public function process(UploadedFileInterface $file): array;

View File

@ -13,4 +13,5 @@ interface Provider
* @throws EmptyResponse
*/
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float;
public function supported(string $money_symbol): bool;
}

View File

@ -3,10 +3,16 @@ namespace Incoviba\Common\Ideal\Cartola;
use Incoviba\Common\Define;
use Incoviba\Common\Ideal\Service;
use Incoviba\Exception\ServiceAction\Read;
use Psr\Http\Message\UploadedFileInterface;
abstract class Banco extends Service implements Define\Cartola\Banco
{
/**
* @param UploadedFileInterface $file
* @return array
* @throws Read
*/
public function process(UploadedFileInterface $file): array
{
$filename = $this->processUploadedFile($file);
@ -40,6 +46,7 @@ abstract class Banco extends Service implements Define\Cartola\Banco
* Process the temp file from getFilename and remove it
* @param string $filename
* @return array
* @throws Read
*/
protected function processFile(string $filename): array
{
@ -88,6 +95,7 @@ abstract class Banco extends Service implements Define\Cartola\Banco
* Translate uploaded file data to database data
* @param string $filename
* @return array
* @throws Read
*/
abstract protected function parseFile(string $filename): array;
}

View File

@ -0,0 +1,16 @@
<?php
namespace Incoviba\Common\Ideal\Money;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Define;
abstract class Provider implements Define\Money\Provider
{
public function __construct(protected LoggerInterface $logger) {}
protected array $supportedMap = [];
public function supported(string $money_symbol): bool
{
return in_array(strtolower($money_symbol), array_keys($this->supportedMap), true);
}
}

View File

@ -45,6 +45,8 @@ abstract class Repository implements Define\Repository
}
/**
* @param int $id
* @return Define\Model
* @throws EmptyResult
*/
public function fetchById(int $id): Define\Model

View File

@ -35,11 +35,11 @@ abstract class API extends Ideal\Service
/**
* @param Define\Model $model
* @param array $new_data
* @param array $newData
* @return Define\Model
* @throws ServiceAction\Update
*/
abstract public function edit(Define\Model $model, array $new_data): Define\Model;
abstract public function edit(Define\Model $model, array $newData): Define\Model;
/**
* @param int $id

View File

@ -10,21 +10,23 @@ class Exception implements ProcessorInterface
public function __invoke(LogRecord $record): LogRecord
{
$context = $record->context;
foreach ($context as $key => $value) {
if (is_a($value, Throwable::class)) {
$exception = $value;
$output = $this->processException($exception);
$context[$key] = $output;
$new_record = new LogRecord(
$record->datetime,
$record->channel,
$record->level,
$record->message,
$context,
$record->extra
);
$record = $new_record;
$changed = false;
array_walk_recursive($context, function (&$item) use (&$changed) {
if (is_a($item, Throwable::class)) {
$item = $this->processException($item);
$changed = true;
}
});
if ($changed) {
$new_record = new LogRecord(
$record->datetime,
$record->channel,
$record->level,
$record->message,
$context,
$record->extra
);
$record = $new_record;
}
if (is_a($record->message, Throwable::class)) {
$exception = $record->message;
@ -56,10 +58,10 @@ class Exception implements ProcessorInterface
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
'trace' => $exception->getTrace(),
];
if ($exception->getPrevious() !== null) {
$output['previous'] = $this->processException($exception);
$output['previous'] = $this->processException($exception->getPrevious());
}
return $output;
}

View File

@ -10,6 +10,7 @@ $app->group('/ventas', function($app) {
include_once $file->getRealPath();
}
$app->get('/add[/]', [Ventas::class, 'add']);
$app->post('/add[/]', [Ventas::class, 'add']);
$app->get('[/]', Ventas::class);
})->add($app->getContainer()->get(Incoviba\Middleware\Authentication::class));
$app->group('/venta/{proyecto_nombre:[A-za-zÑñ\+\ %0-9]+}/{unidad_descripcion:[0-9]+}', function($app) {

View File

@ -338,6 +338,7 @@
data.errors.forEach(errorData => {
console.error(errorData)
})
throw Error('Error al importar cartolas')
}
this.data.movimientos = data.movimientos.map(movimiento => new Movimiento(movimiento))
this.draw().movimientos()

View File

@ -2,7 +2,7 @@
@section('page_content')
<div class="ui container">
<h4 class="ui header">Bienvenid@ {{$user->name}}</h4>
<h4 class="ui header">Bienvenid@ {{ $user->name }}</h4>
<div class="ui basic fitted segment">
<span id="cuotas_hoy"></span>
<span id="cuotas_pendientes"></span>

View File

@ -0,0 +1,4 @@
@push('page_scripts')
<!-- use version 0.20.3 -->
<script type="text/javascript" src="https://cdn.sheetjs.com/xlsx-0.20.3/package/dist/xlsx.full.min.js"></script>
@endpush

View File

@ -5,6 +5,6 @@
@else
<title>Incoviba</title>
@endif
<link rel="icon" href="{{$urls->images}}/Isotipo 16.png" />
<link rel="icon" href="{{ $urls->images }}/Isotipo 16.png" />
@include('layout.head.styles')
</head>

View File

@ -33,12 +33,12 @@
<label for="proyecto">Proyecto</label>
<div class="four wide field">
<div class="ui fluid search selection dropdown" id="proyecto">
<input type="hidden" name="proyecto" />
<input type="hidden" name="proyecto" value="{{ $proyecto_id ?? '' }}" />
<i class="dropdown icon"></i>
<div class="default text">Proyecto</div>
<div class="menu">
@foreach($proyectos as $proyecto)
<div class="item" data-value="{{$proyecto->id}}">{{$proyecto->descripcion}}</div>
<div class="item {{ (isset($proyecto_id) && $proyecto_id == $proyecto->id) ? 'active selected' : '' }}" data-value="{{$proyecto->id}}">{{$proyecto->descripcion}}</div>
@endforeach
</div>
</div>
@ -68,7 +68,7 @@
<label for="valor">Valor</label>
<div class="inline field">
<div class="ui right labeled input">
<input type="text" name="valor" id="valor" required />
<input type="text" name="valor" id="valor" value="{{ $valor ?? '' }}" required />
<div class="ui label">UF</div>
</div>
</div>
@ -145,7 +145,7 @@
</span>
</div>
<button class="ui button" type="submit">Agregar</button>
<button class="ui button" type="submit" id="submit_button">Agregar</button>
</form>
</div>
@endsection
@ -255,14 +255,18 @@
}
class Comuna {
id
data
data = {
region: 0,
provincias: []
}
propietario
promises = {
provincias: null,
comunas: {}
}
constructor(id) {
this.id = id
this.data = {
region: 0,
provincias: []
}
$(this.id).dropdown({
forceSelection: true
@ -272,13 +276,23 @@
get() {
return {
provincias: () => {
if (this.data.region in this.propietario.data.comunas) {
this.promises.provincias = new Promise(resolve => resolve(this.propietario.data.comunas[this.data.region])).then(data => {
this.data.provincias = data.provincias
this.draw().comunas()
})
return this.promises.provincias
}
this.propietario.data.comunas[this.data.region] = {region: this.data.region, provincias: []}
const uri = '{{$urls->api}}/region/' + this.data.region + '/provincias'
return fetchAPI(uri).then(response => {
this.promises.provincias = APIClient.fetch(uri).then(response => {
this.promises.provincias = null
if (response.ok) {
return response.json()
}
}).then(data => {
this.data.provincias = data.provincias
this.propietario.data.comunas[this.data.region].provincias = data.provincias
const promises = []
this.data.provincias.forEach(provincia => {
promises.push(this.get().comunas(provincia.id))
@ -287,17 +301,21 @@
this.draw().comunas()
})
})
return this.data.provincias
},
comunas: provincia_id => {
const uri = '{{$urls->api}}/provincia/' + provincia_id + '/comunas'
return fetchAPI(uri).then(response => {
this.promises.comunas[provincia_id] = fetchAPI(uri).then(response => {
if (response.ok) {
return response.json()
}
}).then(data => {
this.promises.comunas[data.provincia_id] = null
const i = this.data.provincias.findIndex(provincia => provincia.id === data.provincia_id)
this.data.provincias[i].comunas = data.comunas
this.propietario.data.comunas[this.data.region].provincias[i].comunas = data.comunas
})
return this.promises.comunas[provincia_id]
}
}
}
@ -347,6 +365,11 @@
}
class PersonaNatural {
valid
parent
components = {
rut: null,
region: null
}
constructor({valid}) {
this.valid = valid
}
@ -418,9 +441,10 @@
return lines.join("\n")
}
activate() {
new RutHandler({id: '#rut', alert_id: '#alert_rut', valid: this.valid})
this.components.rut = new RutHandler({id: '#rut', alert_id: '#alert_rut', valid: this.valid})
const comuna = new Comuna('#comuna')
new Region({id: '#region', comuna})
comuna.propietario = this.parent
this.components.region = new Region({id: '#region', comuna})
}
}
class PersonaTributaria {
@ -572,6 +596,15 @@
}
class Propietario {
ids
components = {
persona: null
}
data = {
comunas: {}
}
promises = {
propietario: null
}
constructor({id, id_tipo, id_cantidad}) {
this.ids = {
@ -591,8 +624,6 @@
document.getElementById(this.ids.cantidad).disabled = !document.getElementById(this.ids.tipo).checked
this.draw()
$(this.ids.span).find('#rut')
}
get tipo() {
return document.getElementById(this.ids.tipo).checked ? (document.getElementById(this.ids.cantidad).checked ? 2 : 0) : 1
@ -613,7 +644,8 @@
return {
propietario: rut => {
const uri = '{{$urls->api}}/ventas/propietario/' + rut.split('-')[0]
return fetchAPI(uri).then(response => {
this.promises.propietario = APIClient.fetch(uri).then(response => {
this.promises.propietario = null
if (response.ok) {
return response.json()
}
@ -627,7 +659,18 @@
parent.find("[name='numero']").val(data.propietario.direccion.numero)
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)
let persona = this.components.persona
if (!('components' in persona)) {
persona = persona.persona
}
const promise = persona.components.region.comuna.promises.provincias
if (promise === null) {
parent.find('#comuna').dropdown('set selected', data.propietario.direccion.comuna.id)
} else {
promise.then(() => {
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)
@ -645,6 +688,8 @@
}
draw() {
const propietario = this.get().propietario(this.tipo)
this.components.persona = propietario
propietario.parent = this
$(this.ids.span).html(propietario.draw())
propietario.activate()
}
@ -655,6 +700,9 @@
data
unidades
added
promises = {
unidades: null
}
constructor({unidades_id, proyecto_id}) {
this.ids = {
@ -702,7 +750,8 @@
return {
unidades: () => {
const uri = '{{$urls->api}}/proyecto/' + this.data.id + '/unidades'
return fetchAPI(uri).then(response => {
this.promises.unidades = APIClient.fetch(uri).then(response => {
this.promises.unidades = null
if (response.ok) {
return response.json()
}
@ -712,6 +761,7 @@
}
this.unidades = data.unidades
})
return this.promises.unidades
}
}
}
@ -733,6 +783,7 @@
)
)
)
return unidad
}
remove(number) {
number = parseInt(number)
@ -750,6 +801,9 @@
}
class Unidad{
number
components = {
dropdown: null
}
constructor({number}) {
this.number = number
}
@ -772,12 +826,16 @@
})
dropdown.append(menu)
dropdown.dropdown()
this.components.dropdown = dropdown
return dropdown
}
}
class Payment {
ids
components = {
checkbox: null
}
constructor({id, checkbox_id}) {
this.ids = {
@ -785,7 +843,8 @@
checkbox: checkbox_id
}
document.getElementById(this.ids.checkbox).onchange = event => {
this.components.checkbox = document.getElementById(this.ids.checkbox)
this.components.checkbox.onchange = event => {
this.toggle()
}
this.toggle()
@ -807,33 +866,138 @@
console.error(errors)
}
$(document).ready(() => {
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'})
const venta = {
ids: {
form: ''
},
components: {
$date: null,
project: null,
buyer: null,
payments: {},
$form: null
},
fill: {
reservation: () => {
venta.fill.date()
venta.fill.buyer()
venta.fill.project()
venta.fill.payments()
},
date: () => {
const dateString = '{{ isset($date) ? ($date?->format('Y-m-d') ?? '') : '' }}'
if (dateString === '') {
return;
}
const date = new Date(dateString)
date.setDate(date.getDate() + 1)
venta.components.$date.calendar('set date', date)
},
buyer: () => {
const fullRut = '{{ isset($propietario) ? $propietario['full'] : '' }}'
if (fullRut === '') {
return;
}
const buyer = $(venta.ids.buyerId)
const rut = buyer.find('#rut')
rut.val(fullRut)
let persona = venta.components.buyer.components.persona
if (!('components' in persona)) {
persona = persona.persona
}
const promise = persona.components.region.comuna.promises.provincias
if (promise === null) {
venta.fill.buyerRut(buyer, rut)
return;
}
promise.then(() => {
venta.fill.buyerRut(buyer, rut)
})
},
buyerRut: (buyer, rut) => {
rut.trigger('change')
const promise = venta.components.buyer.promises.propietario
if (promise === null) {
venta.fill.buyerData(buyer)
return;
}
promise.then(() => {
venta.fill.buyerData(buyer)
})
},
buyerData: buyer => {
const email = buyer.find('[name="email"]')
if (email.val() === '') {
email.val('{{ $propietario['email'] ?? '' }}')
}
const telefono = buyer.find('[name="telefono"]')
if (telefono.val() === '') {
telefono.val('{{ $propietario['telefono'] ?? '' }}')
}
},
project: () => {
const projectId = {{ isset($proyecto_id) ? $proyecto_id : 0 }};
if (projectId === 0) {
return;
}
$(venta.ids.projectId).dropdown('set selected', projectId);
const payments = [
'pie',
'subsidio',
'credito',
'bono_pie'
]
payments.forEach(payment => {
new Payment({
id: '#' + payment,
checkbox_id: 'has_' + payment
})
})
$('#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 promise = venta.components.project.promises.unidades
if (promise === null) {
venta.fill.units()
return;
}
promise.then(() => {
venta.fill.units()
})
},
units: () => {
const unitString = '{!! isset($unidades) ? json_encode($unidades) : '' !!}';
if (unitString === '') {
return;
}
const unitTypes = JSON.parse(unitString)
Object.entries(unitTypes).forEach(([type, units]) => {
units.forEach(unit_id => {
const unit = venta.components.project.add(type)
unit.components.dropdown.dropdown('set selected', unit_id)
})
})
},
payments: () => {
const formaPagoString = '{!! isset($forma_pago) ? json_encode($forma_pago) : '' !!}';
if (formaPagoString === '') {
return;
}
const formaPago = JSON.parse(formaPagoString)
Object.entries(formaPago).forEach(([key, value]) => {
const payment = venta.components.payments[key]
const event = new Event('change', {
view: window,
bubbles: true,
cancelable: true
})
payment.components.checkbox.dispatchEvent(event)
if (key in venta.fill.payment) {
venta.fill.payment[key](value)
return
}
console.debug(`No fill defined for payment ${key}`)
})
},
payment: {
pie: data => {
document.querySelector('[name="pie"]').value = data.valor
document.querySelector('[name="cuotas"]').value = data.cuotas
},
credito: data => {
document.querySelector('[name="credito"]').value = data
}
}
},
submit: form => {
const body = new FormData(form)
const unidades = form.querySelectorAll("[name^='unidad']")
const emptyUnidades = []
unidades.forEach(unidad => {
if (unidad.value.trim() === '') {
@ -846,7 +1010,7 @@
return;
}
const uri = '{{$urls->api}}/ventas/add'
return fetchAPI(uri, {method: 'post', body}).then(response => {
return APIClient.fetch(uri, {method: 'post', body}).then(response => {
if (!response) {
return false
}
@ -861,6 +1025,74 @@
return false
})
})
},
setup({dateId, buyerId, buyerTypeId, buyerNId, unitsId, projectId, fromReservation = false}) {
this.ids = {
dateId,
buyerId,
buyerTypeId,
buyerNId,
unitsId,
projectId
}
this.components.$date = $(dateId)
const cdo = structuredClone(calendar_date_options)
cdo['maxDate'] = new Date()
this.components.$date.calendar(cdo)
this.components.project = new Proyecto({unidades_id: unitsId, proyecto_id: projectId})
this.components.buyer = new Propietario({id: buyerId, id_tipo: buyerTypeId, id_cantidad: buyerNId})
const payments = [
'pie',
'subsidio',
'credito',
'bono_pie'
]
payments.forEach(payment => {
this.components.payments[payment] = new Payment({
id: '#' + payment,
checkbox_id: 'has_' + payment
})
})
this.components.$form = $(this.ids.form)
this.components.$form.submit(event => {
event.preventDefault();
const button = $(event.currentTarget).find(".ui.button[type='submit']");
button.prop('disabled', true);
button.css('cursor', 'wait');
const form = event.currentTarget;
venta.submit(form);
return false;
})
document.getElementById('submit_button').addEventListener('click', clickEvent => {
clickEvent.preventDefault();
const button = $(clickEvent.currentTarget)
button.prop('disabled', true);
button.css('cursor', 'wait');
const form = document.getElementById('add_form')
venta.submit(form)
return false;
})
if (fromReservation) {
this.fill.reservation()
}
}
}
$(document).ready(() => {
const fromReservation = {{ $from_reservation ? 'true' : 'false' }};
venta.setup({
dateId: '#fecha_venta_calendar',
buyerId: '#propietario',
buyerTypeId: 'persona_propietario',
buyerNId: 'cantidad_propietario',
unitsId: '#unidades',
projectId: '#proyecto',
fromReservation: fromReservation
})
})
</script>

View File

@ -44,6 +44,7 @@
facturas: () => {
document.getElementById(this.ids.facturas).innerHTML = this.venta.draw().facturas(this.formatters)
this.venta.watch().facturas()
$('[data-html]').popup()
}
}
},

View File

@ -220,9 +220,47 @@
},
resumen: ({no, classes, formatters}) => {
const emptyTerreno = '<div class="ui tiny red horizontal circular label">0</div>'
const calculoTerreno = [
[
`Base Terreno (${formatters.date.format(this.props.terreno.fecha)})`,
`\$${formatters.pesos.format(this.props.terreno.valor)}`
],
[
`IPC (${formatters.date.format(this.props.fecha)})`,
`${formatters.percent.format({{ $ipc }})}%`
],
[
'Prorrateo',
`${formatters.percent.format(this.props.detalle.descuento * 100)}%`
],
[
'Proporcion',
`${(this.props.proporcion * 100).toFixed(2).replace('.', ',')}%`
],
[
'Terreno Aplicado',
`\$${formatters.pesos.format(this.props.detalle.terreno)}`
]
]
const tableClasses = [
'',
" align='right'"
]
const calculoTerrenoTable = [
'<table>'
]
calculoTerreno.forEach(items => {
const row = []
items.forEach((cell, idx) => {
row.push(`<td${tableClasses[idx]}>${cell}</td>`)
})
calculoTerrenoTable.push(`<tr>${row.join('')}</tr>`)
})
calculoTerrenoTable.push('</table>')
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 />' +
'Valor con Terreno: $' + formatters.pesos.format(this.props.detalle.base) + ' - Menos valor terreno: <span data-variation="wide multiline" data-html="' + calculoTerrenoTable.join('') + '">$' + ((this.props.detalle.terreno > 0) ? formatters.pesos.format(-this.props.detalle.terreno) : emptyTerreno) + '</span><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 />' +

View File

@ -13,6 +13,31 @@
</div>
</div>
</div>
<div class="ui three column grid">
<div class="column">
<button class="ui button" id="import_template">
<i class="file icon"></i>
Descargar plantilla
</button>
</div>
<div class="column">
<div class="ui checkbox import_filled">
<input type="checkbox" name="filled" />
<label>¿Con unidades del proyecto?</label>
</div>
</div>
<div class="column">
<div class="ui radio checkbox import_format">
<input type="radio" name="format" value="csv" />
<label>CSV</label>
</div>
<br />
<div class="ui radio checkbox import_format">
<input type="radio" name="format" value="xlsx" checked />
<label>XLSX</label>
</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">
@ -32,8 +57,131 @@
</div>
</div>
@include('layout.body.scripts.xlsx')
@push('page_scripts')
<script>
class ImportTemplate {
ids = {
button: '',
filled: '',
format: ''
}
components = {
button: null,
$filled: null,
$format: null
}
data = {
filename: '',
filled: false,
format: '',
columns: [],
csv: {
separator: '',
},
}
download(event) {
event.preventDefault()
if (this.data.format === 'csv') {
this.downloadCsv()
}
if (this.data.format === 'xlsx') {
this.downloadXlsx()
}
return false
}
downloadCsv() {
const data = this.buildCsvData()
const blob = new Blob([data.map(row => row.join(this.data.csv.separator)).join('\n')], {type: 'text/csv'})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = this.data.filename + '.csv'
a.click()
URL.revokeObjectURL(url)
}
downloadXlsx() {
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(this.buildData())
XLSX.utils.book_append_sheet(workbook, worksheet, 'Plantilla de Precios')
XLSX.writeFile(workbook, this.data.filename + '.xlsx', { compression: true })
}
buildData() {
const data = [this.data.columns]
if (this.data.filled) {
const dateFormatter = new Intl.DateTimeFormat('es-CL')
const date = precios.components.modals.import.components.$calendar.calendar('get date')
precios.data.precios.forEach(tipologia => {
tipologia.lineas.forEach(linea => {
linea.unidades.forEach(unidad => {
data.push({
'Fecha*': date ? dateFormatter.format(date) : '',
'Proyecto*': precios.data.proyecto,
'Tipo*': tipologia.tipo,
Unidad: unidad.nombre,
Valor: ''
})
})
})
})
}
return data
}
buildCsvData() {
const data = this.buildData()
const output = []
output.push(Object.keys(data[0]))
data.forEach(row => {
output.push(Object.values(row))
})
return output
}
constructor() {
this.ids.button = 'import_template'
this.ids.filled = 'import_filled'
this.ids.format = 'import_format'
this.data.filename = 'Plantilla de Precios'
this.data.format = 'xlsx'
this.data.columns = {
'Fecha*': 'Fecha es opcional. Se toma la fecha seleccionada',
'Proyecto*': 'Proyecto es opcional. Se toma el proyecto seleccionado',
'Tipo*': 'Tipo es optional. Se toma departamento.',
Unidad: '',
Valor: ''
}
this.data.csv.separator = ';'
this.setup()
}
setup() {
this.components.button = document.getElementById(this.ids.button)
this.components.button.addEventListener('click', this.download.bind(this))
this.components.$filled = $(`.${this.ids.filled}`)
this.components.$filled.checkbox({
fireOnInit: true,
onChange: () => {
const checkbox = this.components.$filled.find('[name="filled"]').toArray()[0]
this.data.filled = checkbox.checked
}
})
this.components.$filled.checkbox('set unchecked')
this.components.$format = $(`.${this.ids.format}`)
this.components.$format.checkbox({
fireOnInit: true,
onChange: () => {
const radios = this.components.$format.find('[name="format"]').toArray()
const checkedRadio = radios.filter(elem => elem.checked)[0]
this.data.format = checkedRadio.value
}
})
}
}
class ImportModal {
ids = {
modal: '',
@ -47,7 +195,8 @@
project: null,
$calendar: null,
file: null,
$file: null
$file: null,
template: null
}
constructor() {
this.ids.modal = 'import_modal'
@ -74,7 +223,9 @@
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('-'))
if (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) {
@ -91,17 +242,23 @@
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.template = new ImportTemplate()
this.components.file = document.getElementById(this.ids.file)
this.components.$file = $(this.components.file.parentNode.querySelector('label'))
this.components.$file.css('cursor', 'pointer')

View File

@ -79,7 +79,7 @@
'<td colspan="3">Todas las Unidades</td>',
`<td class="right aligned">`,
`<button class="ui red icon button remove_button project" data-id="${project.id}">`,
'<i class="plus icon"></i>',
'<i class="trash icon"></i>',
'</button>',
`</td>`
].join("\n")

View File

@ -101,7 +101,14 @@
</div>
</div>
@include('layout.body.scripts.rut')
@include('ventas.reservations.modal.common.scripts.promotions')
@include('ventas.reservations.modal.common.scripts.units')
@include('ventas.reservations.modal.common.scripts.payments')
@include('ventas.reservations.modal.common.scripts.modal')
@include('ventas.reservations.modal.add')
@include('ventas.reservations.modal.edit')
@include('ventas.reservations.modal.comment')
@endsection
@ -123,7 +130,7 @@
}
component_id = ''
component = null
current_project = null;
current_project = null
title_id = ''
title_component = null
@ -158,6 +165,7 @@
reservations.get.reservations(project_id)
reservations.components.modals.add.load(project_id)
reservations.components.modals.edit.load(project_id)
this.hide()
this.title_component.innerHTML = event.currentTarget.innerHTML
@ -294,56 +302,153 @@
return this
}
}
get columnsData() {
return this.reservations.map(reservation => {
const date = new Date(Date.parse(reservation.date) + 24 * 60 * 60 * 1000)
return {
id: reservation.id,
unidades: reservation.summary,
cliente: reservation.buyer.nombreCompleto,
fecha: this.formatters.date.format(date),
oferta: `${this.formatters.ufs.format(reservation.offer)} UF`,
valida: reservation.valid ? '<span class="ui green text">Si</span>' : '<span class="ui red text">No</span>',
operador: reservation.broker?.name ?? '',
}
})
}
draw() {
if (this.reservations.length === 0) {
this.empty()
return
}
const tbody = this.component.querySelector('tbody')
tbody.innerHTML = ''
this.columnsData.forEach(column => {
const tr = document.createElement('tr')
const contents = []
const id = column.id
this.columnNames.forEach(name => {
contents.push(`<td>${column[name]}</td>`)
})
const actions = this.drawActions(id)
if (actions !== '') {
contents.push(actions)
}
tr.innerHTML = contents.join("\n")
tbody.appendChild(tr)
})
this.show()
this.watch()
get columnsData() {
const columnValues = {
id: reservation => reservation.id,
unidades: reservation => reservation.summary,
cliente: reservation => reservation.buyer.nombreCompleto,
fecha: reservation => this.formatters.date.format(new Date(Date.parse(reservation.date) + 24 * 60 * 60 * 1000)),
oferta: reservation => `${this.formatters.ufs.format(reservation.offer)} UF`,
valida: reservation => reservation.valid ? '<span class="ui green text">Si</span>' : '<span class="ui red text">No</span>',
operador: reservation => reservation.broker?.name ?? '',
}
const tooltipVariation = 'very wide multiline'
const tooltips = this.draw().tooltip()
return this.reservations.map(reservation => {
const data = {}
Object.entries(columnValues).forEach(([key, value]) => {
if (key in tooltips) {
const processor = tooltips[key]
const content = processor(reservation)//.replaceAll(' ', '&nbsp;')
data[key] = `data-html="${content}" data-variation="${tooltipVariation}">${value(reservation)}`
return
}
data[key] = value(reservation)
})
return data
})
}
drawActions(id) {
return `
<td class="right aligned">
<button class="ui green mini icon button approve" data-id="${id}" title="Aprobar">
<i class="check icon"></i>
</button>
<button class="ui red mini icon button reject" data-id="${id}" title="Rechazar">
<i class="trash icon"></i>
</button>
</td>`
draw() {
return {
tbody: () => {
if (this.reservations.length === 0) {
this.empty()
return
}
const tbody = this.component.querySelector('tbody')
tbody.innerHTML = ''
this.columnsData.forEach(column => {
const tr = document.createElement('tr')
const contents = []
const id = column.id
this.columnNames.forEach(name => {
let td = `<td>${column[name]}</td>`
if (column[name].includes('data-content') || column[name].includes('data-html')) {
td = `<td ${column[name]}</td>`
}
contents.push(td)
})
const actions = this.draw().actions(id)
if (actions !== '') {
contents.push(actions)
}
tr.innerHTML = contents.join("\n")
tbody.appendChild(tr)
})
this.show()
$(this.component).find('[data-content],[data-html]').popup()
this.watch()
},
actions: id => {
return [
'<td class="right aligned">',
this.draw().actionButtons().edit(id),
this.draw().actionButtons().approve(id),
this.draw().actionButtons().reject(id),
'</td>'
].join("\n")
},
actionButtons: () => {
return {
edit: id => {
return [
`<button class="ui blue mini icon button edit" data-id="${id}" title="Editar">`,
'<i class="edit icon"></i>',
'</button>'
].join("\n")
},
approve: id => {
return [
`<button class="ui green mini icon button approve" data-id="${id}" title="Aprobar">`,
'<i class="check icon"></i>',
'</button>'
].join("\n")
},
reject: id => {
return [
`<button class="ui red mini icon button reject" data-id="${id}" title="Rechazar">`,
'<i class="trash icon"></i>',
'</button>'
].join("\n")
}
}
},
tooltip: () => {
return {
oferta: reservation => {
const formatter = new Intl.NumberFormat('es-CL', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
const output = []
const table = ['<table>']
reservation.units.forEach(unit => {
const type = unit.unit.proyecto_tipo_unidad.tipo_unidad.descripcion
table.push(`<tr><td>${type.charAt(0).toUpperCase() + type.slice(1)} ${unit.unit.descripcion}:</td><td align='right'>${formatter.format(unit.value)} UF</td></tr>`)
})
if (reservation.broker !== null) {
table.push('<tr><td>-----</td></tr>')
let commission = reservation.broker.commission
table.push(`<tr><td>Broker:</td><td align='right'>${reservation.broker.name}</td><td align='right'>(${commission})</td></tr>`)
}
if (reservation.promotions.length > 0) {
table.push('<tr><td>-----</td></tr>')
reservation.promotions.forEach(promotion => {
let value = 0
switch (promotion.type) {
case {{ \Incoviba\Model\Venta\Promotion\Type::FIXED }}:
value = `${formatter.format(promotion.amount)} UF`
break
case {{ \Incoviba\Model\Venta\Promotion\Type::VARIABLE }}:
value = `${formatter.format(promotion.amount * 100)} %`
break
}
table.push(`<tr><td>${promotion.description}:</td><td align='right'>${value}</td></tr>`)
})
}
table.push('</table>')
output.push(table.join(''))
return output.join("<br />")
},
cliente: reservation => {
const formatter = new Intl.NumberFormat('es-CL', {
minimumFractionDigits: 0,
maximumFractionDigits: 0
})
return [
`RUT: ${formatter.format(reservation.buyer.rut)}-${reservation.buyer.digito}`
].join("<br />")
}
}
}
}
}
watch() {
if (Object.keys(this.actions).length === 0) {
return
@ -361,6 +466,7 @@
})
})
}
empty() {
const tbody = this.component.querySelector('tbody')
tbody.innerHTML = ''
@ -371,15 +477,19 @@
this.show()
}
show() {
this.component.style.display = this.display.reservations
}
hide() {
this.component.style.display = 'none'
}
find(id) {
return this.reservations.find(reservation => reservation.id === parseInt(id))
}
send = {
get(url) {
return APIClient.fetch(url).then(response => response.json()).then(json => {
@ -406,28 +516,104 @@
}
get columnsData() {
const data = super.columnsData;
const data = super.columnsData
return data.map(row => {
delete(row['valida'])
delete (row['valida'])
return row
})
}
drawActions(id) {
return `
<td class="right aligned">
<button class="ui green mini icon button edit" data-id="${id}" title="Promesar">
<i class="right chevron icon"></i>
</button>
<button class="ui red mini icon button remove" data-id="${id}" title="Abandonar">
<i class="trash icon"></i>
</button>
</td>`
draw() {
const draw = super.draw()
const actionButtons = draw.actionButtons()
draw.actionButtons = () => {
actionButtons['promise'] = id => {
return [
`<button class="ui green mini icon button promise" data-id="${id}" title="Promesar">`,
'<i class="right chevron icon"></i>',
'</button>'
].join("\n")
}
actionButtons['remove'] = id => {
return [
`<button class="ui red mini icon button remove" data-id="${id}" title="Abandonar">`,
'<i class="trash icon"></i>',
'</button>'
].join("\n")
}
return actionButtons
}
draw.actions = (id) => {
return [
'<td class="right aligned">',
this.draw().actionButtons().edit(id),
this.draw().actionButtons().promise(id),
this.draw().actionButtons().remove(id),
'</td>'
].join("\n")
}
return draw
}
actions = {
edit: event => {
event.preventDefault()
const id = event.currentTarget.dataset.id
reservations.components.modals.edit.load(id)
reservations.components.modals.edit.show({type: 'active', reservation_id: id})
return false
},
promise: event => {
event.preventDefault()
const reservationId = event.currentTarget.dataset.id
const reservation = this.find(reservationId)
// Create a form to submit the data via POST
const form = document.createElement('form')
form.method = 'POST'
form.action = '{{ $urls->base }}/ventas/add'
// Add CSRF token
const csrfToken = Math.random().toString(36).substring(2)
const csrfInput = document.createElement('input')
csrfInput.type = 'hidden'
csrfInput.name = '_token'
csrfInput.value = csrfToken
form.appendChild(csrfInput)
// Add reservation data
const addInput = (name, value) => {
if (value) {
const input = document.createElement('input')
input.type = 'hidden'
input.name = name
input.value = value
form.appendChild(input)
}
}
addInput('from_reservation', 'true')
addInput('reservation_id', reservationId)
if (reservation.cliente) {
addInput('cliente_nombre', reservation.cliente.nombre)
addInput('cliente_rut', reservation.cliente.rut)
addInput('cliente_email', reservation.cliente.email)
addInput('cliente_telefono', reservation.cliente.telefono)
}
if (reservation.propiedad) {
addInput('proyecto_id', reservation.propiedad.proyecto_id)
// Add other property-related parameters as needed
}
if (reservation.oferta) {
addInput('valor', reservation.oferta.valor)
// Add other offer-related parameters as needed
}
// Submit the form
document.body.appendChild(form)
form.submit()
return false
},
remove: event => {
@ -448,7 +634,89 @@
super({component_id, formatters})
}
draw() {
const draw = super.draw()
const tooltip = draw.tooltip()
tooltip['valida'] = reservation => {
const formatter = new Intl.NumberFormat('es-CL', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
const output = []
const table = [
'<table>',
'<thead>',
'<tr>',
'<th></th>',
'<th>Base Oferta</th>',
'<th>Precio</th>',
'</thead>'
]
const rowAlters = [
'',
" align='right'",
" align='right'",
" align='right'"
]
reservation.units.forEach(unit => {
let type = unit.unit.proyecto_tipo_unidad.tipo_unidad.descripcion
type = type.charAt(0).toUpperCase() + type.slice(1)
const base = unit.base ?? (unit.value ?? 0)
const price = unit.unit.current_precio?.valor ?? 0
const diff = (base - price) / price * 100
const rowsData = [
[
`${type} ${unit.unit.descripcion}`,
`${formatter.format(base)} UF`,
`${formatter.format(price)} UF`,
`(${formatter.format(diff)} %)`
]
]
const rowContent = []
rowsData.forEach(row => {
const content = []
row.forEach((cell, idx) => {
content.push(`<td${rowAlters[idx]}>${cell}</td>`)
})
rowContent.push(`<tr>${content.join('')}</tr>`)
})
table.push(rowContent.join(''))
})
if (reservation.broker !== null) {
table.push('<tr><td>-----</td></tr>')
let commission = reservation.broker.commission
table.push(`<tr><td>Broker:</td><td align='right'>${reservation.broker.name}</td><td align='right'>(${commission})</td></tr>`)
}
reservation.promotions.forEach(promotion => {
let value = 0
switch (promotion.type) {
case {{ \Incoviba\Model\Venta\Promotion\Type::FIXED }}:
value = `${formatter.format(promotion.amount)} UF`
break
case {{ \Incoviba\Model\Venta\Promotion\Type::VARIABLE }}:
value = `${formatter.format(promotion.amount * 100)} %`
break
}
table.push(`<tr><td>${promotion.description}:</td><td align='right'>${value}</td></tr>`)
})
table.push('</table>')
output.push(table.join(''))
return output.join("<br />")
}
draw['tooltip'] = () => {
return tooltip
}
return draw
}
actions = {
edit: event => {
event.preventDefault()
const id = event.currentTarget.dataset.id
reservations.components.modals.edit.show({type: 'pending', reservation_id: id})
return false
},
approve: event => {
event.preventDefault()
const id = event.currentTarget.dataset.id
@ -482,10 +750,17 @@
return data[idx]
})
}
drawActions(id) {
return ''
draw() {
const draw = super.draw()
draw.actions = (id) => {
return ''
}
return draw
}
watch() {
}
watch() {}
mapState(state) {
return {
@ -515,6 +790,18 @@
loader: '',
results: '',
},
data: {
comunas: {},
brokers: {},
promotions: {},
units: {}
},
locks: {
comunas: null,
brokers: null,
promotions: null,
units: null
},
get: {
send: (project_id, url_segment, component) => {
const url = `/api/ventas/reservations/project/${project_id}/${url_segment}`
@ -522,7 +809,7 @@
if (json.reservations.length > 0) {
component.set.reservations(json.reservations)
}
component.draw()
component.draw().tbody()
})
},
active: project_id => {
@ -559,7 +846,137 @@
return current_url.pathname.replace(/project\/\d+/, `project/${project_id}`)
}
return `${current_url.pathname}/project/${project_id}`
}
},
comunas: region_id => {
if (region_id in reservations.data.comunas) {
reservations.locks.comunas = null
return new Promise(resolve => {
resolve(reservations.data.comunas[region_id])
})
}
if (reservations.locks.comunas !== null) {
return reservations.locks.comunas
}
const uri = `/api/region/${region_id}/comunas`
reservations.locks.comunas = APIClient.fetch(uri).then(response => response.json()).then(json => {
if (json.comunas.length === 0) {
return
}
reservations.data.comunas[region_id] = json.comunas.map(comuna => {
return {
text: comuna.descripcion,
name: comuna.descripcion,
value: comuna.id
}
})
return reservations.data.comunas[region_id]
})
return reservations.locks.comunas
},
brokers: project_id => {
if (project_id in reservations.data.brokers) {
reservations.locks.brokers = null
return new Promise((resolve, reject) => {
resolve(reservations.data.brokers[project_id])
})
}
if (reservations.locks.brokers !== null) {
return reservations.locks.brokers
}
const uri = `/api/proyecto/${project_id}/brokers`
reservations.locks.brokers = APIClient.fetch(uri).then(response => response.json()).then(json => {
if (json.contracts.length === 0) {
return
}
const formatter = new Intl.NumberFormat('es-CL', { style: 'percent', minimumFractionDigits: 2 })
reservations.data.brokers[project_id] = json.contracts.map(contract => {
return {
id: contract.id,
broker_rut: contract.broker_rut,
commission: formatter.format(contract.commission),
name: '',
text: '',
value: contract.broker_rut
}
})
const promises = []
json.contracts.forEach(contract => {
promises.push(reservations.get.broker(contract.broker_rut))
})
return Promise.all(promises).then(data => {
data.forEach(broker => {
if (!('rut' in broker)) {
return
}
const idx = reservations.data.brokers[project_id].findIndex(contract => contract.broker_rut === broker.rut)
const text = reservations.data.brokers[project_id][idx].text = `${broker.name} - ${reservations.data.brokers[project_id][idx].commission}`
reservations.data.brokers[project_id][idx].name = text
reservations.data.brokers[project_id][idx].text = text
})
})
})
return reservations.locks.brokers
},
broker: (broker_rut) => {
const uri = `/api/proyectos/broker/${broker_rut}`
return APIClient.fetch(uri).then(response => response.json()).then(json => {
if (!('broker' in json)) {
return []
}
return json.broker
})
},
promotions: project_id => {
if (project_id in reservations.data.promotions) {
reservations.locks.promotions = null
return new Promise((resolve, reject) => {
resolve(this.data.promotions[project_id])
})
}
if (reservations.locks.promotions !== null) {
return reservations.locks.promotions
}
const uri = `/api/proyecto/${project_id}/promotions`
reservations.locks.promotions = APIClient.fetch(uri).then(response => response.json()).then(json => {
if (json.promotions.length === 0) {
return reservations.data.promotions[project_id] = []
}
return reservations.data.promotions[project_id] = json.promotions
})
return reservations.locks.promotions
},
units: project_id => {
if (project_id in reservations.data.units) {
reservations.locks.units = null
return new Promise((resolve, reject) => {
resolve(reservations.data.units[project_id])
})
}
if (reservations.locks.units !== null) {
return reservations.locks.units
}
const uri = `/api/proyecto/${project_id}/unidades/disponibles`
reservations.locks.units = APIClient.fetch(uri).then(response => response.json()).then(json => {
if (json.unidades.length === 0) {
reservations.data.units[project_id] = {}
return reservations.data.units[project_id]
}
if (!(project_id in reservations.data.units)) {
reservations.data.units[project_id] = {}
}
json.unidades.forEach(unit => {
const type = unit.proyecto_tipo_unidad.tipo_unidad.descripcion
if (!(type in reservations.data.units[project_id])) {
reservations.data.units[project_id][type] = []
}
reservations.data.units[project_id][type].push(unit)
})
return reservations.data.units[project_id]
})
return reservations.locks.units
},
},
loading: {
show: () => {
@ -648,6 +1065,7 @@
this.show.projects()
this.components.modals.add = new AddReservationModal(configuration.ids.projects)
this.components.modals.edit = new EditReservationModal(configuration.ids.projects)
this.components.modals.comment = new CommentModal()
const project_id = {{ $project_id ?? 'null' }};

View File

@ -1,400 +1,99 @@
<div class="ui modal" id="add_reservation_modal">
<div class="header">
Agregar Cierre
</div>
<div class="content">
<form class="ui form" id="add_reservation_form">
<input type="hidden" name="add_project_id" />
<div class="three wide required field">
<label>Fecha</label>
<div class="ui calendar" id="add_date">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="add_date" />
</div>
</div>
</div>
<div class="fields">
<div class="three wide required field">
<label>RUT</label>
<div class="ui right labeled input" id="add_rut">
<input type="text" name="add_buyer_rut" placeholder="RUT" />
<div class="ui basic label">-<span id="add_digit"></span></div>
</div>
</div>
<div class="field">
<label></label>
<div class="ui inline loader" id="add_rut_loader"></div>
</div>
</div>
<div class="fields">
<div class="three wide required field">
<label>Nombre</label>
<input type="text" name="add_buyer_name" placeholder="Nombre" />
</div>
<div class="six wide required field">
<label>Apellidos</label>
<input type="text" name="add_buyer_last_name" placeholder="Apellido Paterno" />
</div>
<div class="six wide field">
<label></label>
<input type="text" name="add_buyer_last_name2" placeholder="Apellido Materno" />
</div>
</div>
<div class="fields">
<div class="three wide field">
<label>Dirección</label>
<input type="text" name="add_buyer_address_street" placeholder="Calle" />
</div>
<div class="field">
<label></label>
<input type="text" name="add_buyer_address_number" placeholder="" />
</div>
<div class="three wide field">
<label></label>
<input type="text" name="add_buyer_address_extra" placeholder="Otros Detalles" />
</div>
</div>
<div class="fields">
<div class="three wide field">
<label>Comuna</label>
<div class="ui search selection dropdown" id="add_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="seven wide field">
<label>Región</label>
<div class="ui search selection dropdown" id="add_region">
<input type="hidden" name="region" />
<i class="dropdown icon"></i>
<div class="default text">Región</div>
<div class="menu">
@foreach($regions as $region)
<div class="item" data-value="{{$region->id}}">{{$region->numeral}} - {{$region->descripcion}}</div>
@endforeach
</div>
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Telefono</label>
<input type="text" name="add_buyer_phone" placeholder="Telefono" />
</div>
<div class="field">
<label>Correo</label>
<div class="ui labeled input">
<input type="text" name="add_buyer_email_name" placeholder="Correo" />
<div class="ui basic label">@</div>
<input type="text" name="add_buyer_email_domain" placeholder="Dominio" />
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Estado Civil</label>
<input type="text" name="add_buyer_marital_status" placeholder="Estado Civil" />
</div>
<div class="field">
<label>Profesión</label>
<input type="text" name="add_buyer_profession" placeholder="Profesión" />
</div>
<div class="field">
<label>Fecha de Nacimiento</label>
<div class="ui calendar" id="add_birthdate">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="birthdate" />
</div>
</div>
</div>
</div>
<div class="six wide field">
<label>Operador *</label>
<div class="ui clearable search selection dropdown" id="add_broker">
<input type="hidden" name="add_broker" />
<i class="dropdown icon"></i>
<div class="default text">Operador</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>Agregar Promoción</label>
<button type="button" class="ui icon button" id="add_promotion">
<i class="plus icon"></i>
</button>
</div>
<div id="add_promotions"></div>
<h4 class="ui dividing header">Unidades <span id="add_project_name"></span></h4>
<div class="fields" id="add_unit_buttons"></div>
<div id="add_units"></div>
</form>
</div>
<div class="actions">
<div class="ui cancel button">
Cancelar
</div>
<div class="ui green ok button">
Agregar
</div>
</div>
</div>
@include('layout.body.scripts.rut')
@include('ventas.reservations.modal.common.modal', ['prefix' => 'add', 'modalTitle' => 'Agregar Cierre', 'okText' => 'Agregar'])
@push('page_scripts')
<script>
class AddModalPromotions {
ids = {
button: '',
elements: ''
}
data = {
promotions: []
}
components = {
button: null,
promotions: null,
}
display = {
button: ''
}
class AddModalPromotions extends ModalPromotions {
constructor() {
this.ids = {
button: 'add_promotion',
elements: 'add_promotions'
}
this.setup()
}
add() {
const idx = Math.max(this.data.promotions.length, 0, Math.max(...this.data.promotions) + 1)
this.data.promotions.push(idx)
this.draw.promotions()
}
reset() {
this.data.promotions = []
this.draw.promotions()
}
remove(idx) {
this.data.promotions = this.data.promotions.filter(promotion => promotion !== idx)
this.draw.promotions()
}
draw = {
promotion: idx => {
const promotions = this.data.promotions.map(promotion => {
return `<div class="item" data-value="${promotion.id}">${promotion.name}</div>`
})
return [
`<div class="fields promotion" data-id="${idx}">`,
'<div class="three wide field">',
'<label>Promoción</label>',
`<div class="ui search selection dropdown">`,
'<input type="hidden" name="add_promotions[]" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Promoción</div>',
`<div class="menu">${promotions.join('')}</div>`,
'</div>',
'</div>',
'<div class="two wide field">',
'<label></label>',
`<button class="ui red tiny icon button remove_promotion" type="button" data-id="${idx}"><i class="trash icon"></i></button>`,
'</div>',
'</div>'
].join('')
},
promotions: () => {
if (this.data.promotions.length === 0) {
this.components.button.parentElement.style.display = 'none'
this.components.promotions.innerHTML = ''
return
}
this.components.button.parentElement.style.display = this.display.button
this.components.promotions.innerHTML = this.data.promotions.map((promotion, idx) => {
return this.draw.promotion(idx)
}).join('')
this.components.promotions.querySelectorAll('.dropdown').forEach(dropdown => {
$(dropdown).dropdown()
})
this.components.promotions.querySelectorAll('.remove_promotion').forEach(button => {
button.addEventListener('click', () => {
const idx = Number(button.dataset.id)
this.remove(idx)
})
})
}
}
setup() {
this.components.button = document.getElementById(this.ids.button)
this.components.promotions = document.getElementById(this.ids.elements)
this.components.button.addEventListener('click', () => {
this.add()
})
this.display.button = this.components.button.parentElement.style.display
this.draw.promotions()
super('add')
}
}
class AddModalUnits {
parent = null
ids = {
buttons_holder: '',
units: ''
class AddModalUnits extends ModalUnits {
constructor() {
super({prefix: 'add', parent: null});
}
data = {
button_map: {},
types: {},
units: [],
}
components = {
buttons_holder: null,
units: null,
}
class AddModalPayments extends ModalPayments {
constructor() {
super('add');
}
constructor(parent) {
this.parent = parent
this.ids = {
buttons_holder: 'add_unit_buttons',
units: 'add_units'
}
this.data.button_map = {
'departamento': 'building',
'estacionamiento': 'car',
'bodega': 'warehouse',
'terraza': 'vector square'
}
this.setup()
}
class AddReservationModal extends ReservationModal {
constructor(projects_id) {
super({projects_id, prefix: 'add', components: {
promotions: new AddModalPromotions(),
units: new AddModalUnits(),
payments: new AddModalPayments()
}});
}
get promotions() {
return this.parent.data.promotions[this.parent.data.current_project]
}
draw = {
button: type => {
return [
'<div class="field">',
`<button class="ui icon button" type="button" data-type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}">`,
'<i class="plus icon"></i>',
`<i class="${this.data.button_map[type]} icon"></i>`,
'</button>',
'</div>'
].join('')
},
buttons: () => {
this.components.buttons_holder.innerHTML = Object.keys(this.data.types).map(type => {
return this.draw.button(type)
}).join('')
this.components.buttons_holder.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', () => {
const type = button.dataset.type
this.add(type)
})
})
},
units: () => {
if (this.data.units.length === 0) {
this.components.units.innerHTML = ''
return
add() {
const url = '/api/ventas/reservation/add'
const form = document.getElementById(this.ids.form)
let body = new FormData(form)
const date = this.components.$date.calendar('get date')
body.set(`${this.prefix}_date`, [date.getFullYear(), date.getMonth() + 1, date.getDate()].join('-'))
body.set(`${this.prefix}_buyer_rut`, Rut.clean(this.components.rut.querySelector('input').value))
body.set(`${this.prefix}_buyer_digit`, this.components.digit.textContent)
body.set(`${this.prefix}_buyer_address_comuna_id`, this.components.$comuna.dropdown('get value'))
body.set(`${this.prefix}_broker_rut`, Rut.clean(this.components.$broker.dropdown('get value')))
body.set(`${this.prefix}_buyer_email`, form.querySelector(`input[name='${this.prefix}_buyer_email_name']`).value + '@' + form.querySelector(`input[name='${this.prefix}_buyer_email_domain']`).value)
const birthdate = this.components.$birthdate.calendar('get date')
body.set(`${this.prefix}_buyer_birthdate`, [birthdate.getFullYear(), birthdate.getMonth() + 1, birthdate.getDate()].join('-'))
if (this.components.payments.components.pie.$checkbox.checkbox('is unchecked')) {
body.delete(`${this.prefix}_payment_pie`)
body.delete(`${this.prefix}_payment_cuotas`)
}
body.delete(`${this.prefix}_payment_has_pie`)
if (this.components.payments.components.credit.$checkbox.checkbox('is unchecked')) {
body.delete(`${this.prefix}_payment_credit`)
}
body.delete(`${this.prefix}_payment_has_credit`)
body.delete('comuna')
body.delete('region')
body.delete('broker')
body.delete(`${this.prefix}_broker`)
body.delete('birthdate')
body.delete(`${this.prefix}_buyer_email_name`)
body.delete(`${this.prefix}_buyer_email_domain`)
body.keys().forEach(key => {
if (body.get(key) === '') {
body.delete(key)
}
this.components.units.innerHTML = this.data.units.map(unit => {
return this.draw.unit(unit)
}).join('')
this.components.units.querySelectorAll('.dropdown.add_units').forEach(dropdown => {
$(dropdown).dropdown({
onChange: (value, text, $selectedItem) => {
const unitPromotions = this.promotions.filter(promotion => promotion.units.length > 0)
const promotions = unitPromotions.filter(promotion => promotion.units.filter(unit => unit.id === parseInt(value)).length > 0)
$selectedItem.parent().parent().parent().parent().find('.add_promotions_unit')
.dropdown('change values', promotions.map(promotion => {
return {
value: promotion.id,
name: promotion.description,
text: promotion.description,
}
}))
}
})
})
this.components.units.querySelectorAll('.dropdown.add_promotions_unit').forEach(dropdown => {
$(dropdown).dropdown()
})
this.components.units.querySelectorAll('.remove_unit').forEach(button => {
button.addEventListener('click', () => {
const idx = Number(button.dataset.id)
this.remove(idx)
})
})
},
unit: unit => {
let promotions = ''
if (unit.type === 'departamento') {
if (this.promotions.filter(promotion => promotion.units.length > 0).length > 0) {
promotions = [
'<div class="three wide field">',
'<label>Promociones</label>',
'<div class="ui multiple search selection dropdown add_promotions_unit">',
'<input type="hidden" name="add_units_promotions[]" />',
'<i class="dropdown icon"></i>',
'<div class="default text">Promociones</div>',
'<div class="menu">',
'</div>',
'</div>',
'</div>'
].join('')
}
}
return [
'<div class="fields">',
'<div class="four wide field">',
`<label>${unit.type.charAt(0).toUpperCase() + unit.type.slice(1)}</label>`,
`<div class="ui search selection dropdown add_units">`,
'<input type="hidden" name="add_units[]" />',
'<i class="dropdown icon"></i>',
`<div class="default text">${unit.type.charAt(0).toUpperCase() + unit.type.slice(1)}</div>`,
'<div class="menu">',
this.data.types[unit.type].map(unit => {
return `<div class="item" data-value="${unit.value}">${unit.name}</div>`
}).join(''),
'</div>',
'</div>',
'</div>',
'<div class="three wide field">',
'<label>Valor</label>',
'<div class="ui right labeled input">',
'<input type="number" name="add_units_value[]" placeholder="Valor" />',
'<div class="ui basic label">UF</div>',
'</div>',
'</div>',
promotions,
'<div class="field">',
'<label></label>',
`<button class="ui red tiny icon button remove_unit" type="button" data-id="${unit.idx}"><i class="trash icon"></i></button>`,
'</div>',
'</div>',
].join('')
})
body = this.validateBody(body)
if (body.keys().toArray().length === 0) {
alert('No hay datos.')
return
}
}
reset() {
this.data.units = []
this.draw.units()
}
add(type) {
const idx = Math.max(this.data.units.length, 0, Math.max(...this.data.units.map(unit => unit.idx)) + 1)
this.data.units.push({idx, type})
this.draw.units()
}
remove(idx) {
this.data.units = this.data.units.filter(unit => unit.idx !== idx)
this.draw.units()
const method = 'post'
return APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (json.success) {
window.location.reload()
}
})
}
setup() {
this.components.buttons_holder = document.getElementById(this.ids.buttons_holder)
this.components.units = document.getElementById(this.ids.units)
this.draw.buttons()
super.setup()
this.components.$modal.modal({
onApprove: () => {
this.add()
}
})
this.components.form.addEventListener('submit', event => {
event.preventDefault()
this.add()
return false
})
}
}
class AddReservationModal {
/*class AddReservationModal {
ids = {
modal: '',
form: '',
@ -429,6 +128,7 @@
project_name: null,
unit_buttons: null,
units: null,
payments: null,
$loader: null
}
data = {
@ -480,8 +180,8 @@
this.get.promotions(project_id).then(promotions => {
this.components.promotions.data.promotions = promotions.map(promotion => {
return {
text: promotion.name,
name: promotion.name,
text: promotion.description,
name: promotion.description,
value: promotion.id
}
})
@ -509,6 +209,7 @@
this.components.form.reset()
this.components.promotions.reset()
this.components.units.reset()
this.components.payments.reset()
}
add() {
const url = '/api/ventas/reservation/add'
@ -524,6 +225,16 @@
const birthdate = this.components.$birthdate.calendar('get date')
body.set('add_buyer_birthdate', [birthdate.getFullYear(), birthdate.getMonth() + 1, birthdate.getDate()].join('-'))
if (this.components.payments.components.pie.$checkbox.checkbox('is unchecked')) {
body.delete('add_pie')
body.delete('add_cuotas')
}
body.delete('add_has_pie')
if (this.components.payments.components.credit.$checkbox.checkbox('is unchecked')) {
body.delete('add_credit')
}
body.delete('add_has_credit')
body.delete('comuna')
body.delete('region')
body.delete('broker')
@ -672,6 +383,9 @@
}
fill = {
user: user => {
if (typeof user === 'undefined' || user === null) {
return
}
const form = this.components.form
form.querySelector('input[name="add_buyer_name"]').value = user.nombres || ''
form.querySelector('input[name="add_buyer_last_name"]').value = user.apellidoPaterno || ''
@ -753,6 +467,7 @@
this.components.projects = document.getElementById(this.ids.projects)
this.components.project_name = document.getElementById(this.ids.project_name)
this.components.units = new AddModalUnits(this)
this.components.payments = new AddModalPayments()
this.components.$modal.modal({
onApprove: () => {
@ -799,6 +514,6 @@
this.components.$broker.dropdown()
this.components.$loader = $(`#${this.ids.loader}`)
}
}
}*/
</script>
@endpush

View File

@ -0,0 +1,164 @@
<form class="ui form" id="{{ $prefix }}_reservation_form">
<input type="hidden" name="{{ $prefix }}_project_id" />
<div class="three wide required field">
<label>Fecha</label>
<div class="ui calendar" id="{{ $prefix }}_date">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="{{ $prefix }}_date" />
</div>
</div>
</div>
<div class="fields">
<div class="three wide required field">
<label>RUT</label>
<div class="ui right labeled input" id="{{ $prefix }}_rut">
<input type="text" name="{{ $prefix }}_buyer_rut" placeholder="RUT" />
<div class="ui basic label">-<span id="{{ $prefix }}_digit"></span></div>
</div>
</div>
<div class="field">
<label></label>
<div class="ui inline loader" id="{{ $prefix }}_rut_loader"></div>
</div>
</div>
<div class="fields">
<div class="three wide required field">
<label>Nombre</label>
<input type="text" name="{{ $prefix }}_buyer_name" placeholder="Nombre" />
</div>
<div class="six wide required field">
<label>Apellidos</label>
<input type="text" name="{{ $prefix }}_buyer_last_name" placeholder="Apellido Paterno" />
</div>
<div class="six wide field">
<label></label>
<input type="text" name="{{ $prefix }}_buyer_last_name2" placeholder="Apellido Materno" />
</div>
</div>
<div class="fields">
<div class="three wide field">
<label>Dirección</label>
<input type="text" name="{{ $prefix }}_buyer_address_street" placeholder="Calle" />
</div>
<div class="field">
<label></label>
<input type="text" name="{{ $prefix }}_buyer_address_number" placeholder="" />
</div>
<div class="three wide field">
<label></label>
<input type="text" name="{{ $prefix }}_buyer_address_extra" placeholder="Otros Detalles" />
</div>
</div>
<div class="fields">
<div class="three wide field">
<label>Comuna</label>
<div class="ui search selection dropdown" id="{{ $prefix }}_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="seven wide field">
<label>Región</label>
<div class="ui search selection dropdown" id="{{ $prefix }}_region">
<input type="hidden" name="region" />
<i class="dropdown icon"></i>
<div class="default text">Región</div>
<div class="menu">
@foreach($regions as $region)
<div class="item" data-value="{{$region->id}}">{{$region->numeral}} - {{$region->descripcion}}</div>
@endforeach
</div>
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Teléfono</label>
<input type="text" name="{{ $prefix }}_buyer_phone" placeholder="Telefono" />
</div>
<div class="field">
<label>Correo</label>
<div class="ui labeled input">
<input type="text" name="{{ $prefix }}_buyer_email_name" placeholder="Correo" />
<div class="ui basic label">@</div>
<input type="text" name="{{ $prefix }}_buyer_email_domain" placeholder="Dominio" />
</div>
</div>
</div>
<div class="fields">
<div class="field">
<label>Estado Civil</label>
<input type="text" name="{{ $prefix }}_buyer_marital_status" placeholder="Estado Civil" />
</div>
<div class="field">
<label>Profesión</label>
<input type="text" name="{{ $prefix }}_buyer_profession" placeholder="Profesión" />
</div>
<div class="field">
<label>Fecha de Nacimiento</label>
<div class="ui calendar" id="{{ $prefix }}_birthdate">
<div class="ui icon input">
<i class="calendar icon"></i>
<input type="text" name="birthdate" />
</div>
</div>
</div>
</div>
<div class="six wide field">
<label>Operador *</label>
<div class="ui clearable search selection dropdown" id="{{ $prefix }}_broker">
<input type="hidden" name="{{ $prefix }}_broker" />
<i class="dropdown icon"></i>
<div class="default text">Operador</div>
<div class="menu"></div>
</div>
</div>
<div class="field">
<label>Agregar Promoción</label>
<button type="button" class="ui icon button" id="{{ $prefix }}_promotion">
<i class="plus icon"></i>
</button>
</div>
<div id="{{ $prefix }}_promotions"></div>
<h4 class="ui dividing header">Unidades <span id="{{ $prefix }}_project_name"></span></h4>
<div class="fields" id="{{ $prefix }}_unit_buttons"></div>
<div id="{{ $prefix }}_units"></div>
<h4 class="ui dividing header">Forma de Pago</h4>
<div class="fields">
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="{{ $prefix }}_payment_has_pie" id="{{ $prefix }}_payment_has_pie" />
<label>¿Tiene Pie?</label>
</div>
</div>
<div class="field" id="{{ $prefix }}_payment_pie">
<label>Pie</label>
<div class="ui right labeled input">
<input type="text" name="{{ $prefix }}_payment_pie" />
<div class="ui basic label">UF</div>
</div>
</div>
<div class="field" id="{{ $prefix }}_payment_cuotas">
<label>Cuotas</label>
<input type="text" name="{{ $prefix }}_payment_cuotas" />
</div>
</div>
<div class="fields">
<div class="field">
<div class="ui checkbox">
<input type="checkbox" name="{{ $prefix }}_payment_has_credit" id="{{ $prefix }}_payment_has_credit" />
<label>¿Tiene Crédito?</label>
</div>
</div>
<div class="field" id="{{ $prefix }}_payment_credit">
<label>Crédito</label>
<div class="ui right labeled input">
<input type="text" name="{{ $prefix }}_payment_credit" />
<div class="ui basic label">UF</div>
</div>
</div>
</div>
</form>

View File

@ -0,0 +1,16 @@
<div class="ui modal" id="{{ $prefix }}_reservation_modal">
<div class="header">
{{ $modalTitle }}
</div>
<div class="content">
@include('ventas.reservations.modal.common.form', ['prefix' => $prefix])
</div>
<div class="actions">
<div class="ui cancel button">
Cancelar
</div>
<div class="ui green ok button">
{{ $okText }}
</div>
</div>
</div>

View File

@ -0,0 +1,373 @@
@push('page_scripts')
<script>
class ReservationModal {
prefix
ids = {
modal: '',
form: '',
date: '',
rut: '',
digit: '',
birthdate: '',
comuna: '',
region: '',
broker: '',
promotion_button: '',
promotions: '',
projects: '',
project_name: '',
unit_buttons: '',
units: ''
}
components = {
$modal: null,
form: null,
$date: null,
rut: null,
digit: null,
$birthdate: null,
$comuna: null,
$region: null,
$broker: null,
promotion_button: null,
promotions: null,
projects: null,
project_name: null,
unit_buttons: null,
units: null,
payments: null,
$loader: null
}
data = {
current_project: null,
comunas: {},
brokers: {},
promotions: {},
unit_buttons: [],
units: {},
added_units: {},
current_user: null
}
maps = {
unit_types: {
departamento: 'building',
estacionamiento: 'car',
bodega: 'warehouse',
terraza: 'tree',
}
}
constructor({projects_id, prefix, components}) {
this.prefix = prefix
this.ids = {
modal: `${this.prefix}_reservation_modal`,
form: `${this.prefix}_reservation_form`,
date: `${this.prefix}_date`,
rut: `${this.prefix}_rut`,
digit: `${this.prefix}_digit`,
birthdate: `${this.prefix}_birthdate`,
comuna: `${this.prefix}_comuna`,
region: `${this.prefix}_region`,
broker: `${this.prefix}_broker`,
promotion_button: `${this.prefix}_promotion`,
promotions: `${this.prefix}_promotions`,
projects: projects_id,
project_name: `${this.prefix}_project_name`,
unit_buttons: `${this.prefix}_unit_buttons`,
units: `${this.prefix}_units`,
loader: `${this.prefix}_rut_loader`
}
Object.keys(components).forEach(key => {
this.components[key] = components[key]
})
this.setup()
}
load(project_id) {
this.reset()
this.data.current_project = project_id
this.components.project_name.textContent = this.components.projects.querySelector(`.item[data-id="${project_id}"]`).textContent
this.components.form.querySelector(`input[name="${this.prefix}_project_id"]`).value = project_id
this.get.brokers(project_id)
this.get.promotions(project_id).then(() => {
const promotions = structuredClone(reservations.data.promotions[project_id])
this.components.promotions.data.promotions = promotions.map(promotion => {
return {
text: promotion.description,
name: promotion.description,
value: promotion.id
}
})
this.components.promotions.draw.promotions()
})
this.get.units(project_id).then(() => {
const unitTypes = structuredClone(reservations.data.units[project_id])
Object.entries(unitTypes).map(([type, units]) => {
units = units.map(unit => {
return {
text: unit.descripcion,
name: unit.descripcion,
value: unit.id
}
})
units.sort((a, b) => {
return parseInt(a.text) - parseInt(b.text)
})
unitTypes[type] = units
})
this.components.units.data.types = unitTypes
this.components.units.draw.buttons()
})
}
reset() {
this.components.form.reset()
this.components.promotions.reset()
this.components.units.reset()
this.components.payments.reset()
}
get = {
comunas: region_id => {
if (region_id in this.data.comunas) {
this.components.$comuna.dropdown('change values', this.data.comunas[region_id])
if (this.data.current_user !== null && this.data.current_user?.direccion?.comuna !== null) {
this.components.$comuna.dropdown('set selected', this.data.current_user.direccion.comuna.id)
}
return
}
return reservations.get.comunas(region_id).then(comunas => {
this.data.comunas[region_id] = comunas
this.components.$comuna.dropdown('change values', this.data.comunas[region_id])
if (this.data.current_user !== null && this.data.current_user?.direccion?.comuna !== null) {
this.components.$comuna.dropdown('set selected', this.data.current_user.direccion.comuna.id)
}
})
},
brokers: project_id => {
if (project_id in this.data.brokers) {
return new Promise((resolve, reject) => {
resolve(this.data.brokers[project_id])
})
}
return reservations.get.brokers(project_id).then(() => {
this.data.brokers[project_id] = reservations.data.brokers[project_id]
this.fill.brokers()
})
},
/*broker: (broker_rut) => {
const uri = `/api/proyectos/broker/${broker_rut}`
return APIClient.fetch(uri).then(response => response.json()).then(json => {
if (!('broker' in json)) {
return []
}
return json.broker
})
},*/
promotions: project_id => {
if (project_id in this.data.promotions) {
return new Promise((resolve, reject) => {
resolve(this.data.promotions[project_id])
})
}
return reservations.get.promotions(project_id).then(promotions => {
this.data.promotions[project_id] = promotions
})
},
units: project_id => {
if (project_id in this.data.units) {
return new Promise((resolve, reject) => {
resolve(this.data.units[project_id])
})
}
return reservations.get.units(project_id).then(units => {
this.data.units[project_id] = units
})
},
user: rut => {
if (this.data.current_user !== null && this.data.current_user?.rut === rut) {
return this.data.current_user
}
this.loader.show()
const uri = `/api/persona/${rut}`
return APIClient.fetch(uri).then(response => response.json()).then(json => {
this.loader.hide()
if (!json.success) {
return
}
this.data.current_user = json.persona
return json.persona
})
}
}
fill = {
user: user => {
if (typeof user === 'undefined' || user === null) {
return
}
const form = this.components.form
form.querySelector(`input[name="${this.prefix}_buyer_name"]`).value = user.nombres || ''
form.querySelector(`input[name="${this.prefix}_buyer_last_name"]`).value = user.apellidoPaterno || ''
form.querySelector(`input[name="${this.prefix}_buyer_last_name2"]`).value = user.apellidoMaterno || ''
form.querySelector(`input[name="${this.prefix}_buyer_address_street"]`).value = user.datos?.direccion?.calle || ''
form.querySelector(`input[name="${this.prefix}_buyer_address_number"]`).value = user.datos?.direccion?.numero || ''
form.querySelector(`input[name="${this.prefix}_buyer_address_extra"]`).value = user.datos?.direccion?.extra || ''
if (parseInt(this.components.$region.dropdown('get value')) !== user.datos?.direccion?.comuna?.provincia?.region?.id) {
this.components.$region.dropdown('set selected', user.datos?.direccion?.comuna?.provincia?.region?.id)
} else {
this.components.$comuna.dropdown('set selected', user.datos?.direccion?.comuna?.id)
}
form.querySelector(`input[name="${this.prefix}_buyer_phone"]`).value = user.datos?.telefono || ''
const email_parts = user.datos?.email?.split('@') || []
form.querySelector(`input[name="${this.prefix}_buyer_email_name"]`).value = email_parts[0] || ''
form.querySelector(`input[name="${this.prefix}_buyer_email_domain"]`).value = email_parts[1] || ''
if (user.datos !== null && 'estadoCivil' in user.datos) {
form.querySelector(`input[name="${this.prefix}_buyer_marital_status"]`).value = user.datos?.estadoCivil.charAt(0).toUpperCase() + user.datos?.estadoCivil.slice(1)
} else {
form.querySelector(`input[name="${this.prefix}_buyer_marital_status"]`).value = ''
}
if (user.datos !== null &&'ocupacion' in user.datos) {
form.querySelector(`input[name="${this.prefix}_buyer_profession"]`).value = user.datos?.ocupacion.charAt(0).toUpperCase() + user.datos?.ocupacion.slice(1).toLowerCase()
} else {
form.querySelector(`input[name="${this.prefix}_buyer_profession"]`).value = ''
}
if (user.datos !== null &&'fechaNacimiento' in user.datos) {
this.components.$birthdate.calendar('set date', user.datos?.fechaNacimiento)
}
},
brokers: () => {
this.components.$broker.dropdown('change values', this.data.brokers[this.data.current_project])
},
units: () => {
const buttons = []
Object.entries(this.maps.unit_types).forEach(([type, map]) => {
if (!(type in this.data.units[this.data.current_project])) {
return
}
buttons.push(`<div class="field"><div class="ui icon button" data-type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}"><i class="plus icon"></i><i class="${map} icon"></i></div></div>`)
})
this.components.unit_buttons.innerHTML = buttons.join('')
this.components.unit_buttons.querySelectorAll('.ui.icon.button').forEach(button => {
button.addEventListener('click', () => {
this.units.add(button.dataset.type)
})
})
}
}
watch = {
region: (value, text, $choice) => {
this.get.comunas(value)
},
}
loader = {
show: () => {
this.components.$loader.show()
},
hide: () => {
this.components.$loader.hide()
}
}
validateBody(body) {
const fieldMap = {
'project_id': this.reservation.project_id,
'date': this.reservation.date,
'buyer_rut': this.reservation.buyer.rut.toString(),
'buyer_digit': this.reservation.buyer.digito,
'buyer_name': this.reservation.buyer.nombres,
'buyer_last_name': this.reservation.buyer.apellidoPaterno,
'buyer_last_name2': this.reservation.buyer.apellidoMaterno,
'buyer_address_street': this.reservation.buyer.datos?.direccion?.calle,
'buyer_address_number': this.reservation.buyer.datos?.direccion?.numero?.toString(),
'buyer_address_extra': this.reservation.buyer.datos?.direccion?.extra,
'buyer_address_comuna_id': this.reservation.buyer.datos?.direccion?.comuna?.id?.toString(),
'buyer_phone': this.reservation.buyer.datos?.telefono,
'buyer_email': this.reservation.buyer.datos?.email,
'buyer_birthdate': this.reservation.buyer.datos?.fechaNacimiento,
'buyer_marital_status': this.reservation.buyer.datos?.estadoCivil,
'buyer_profession': this.reservation.buyer.datos?.ocupacion,
'broker_rut': this.reservation.broker?.rut?.toString(),
'payment_pie': this.reservation.payment?.pie?.valor?.toString(),
'payment_cuotas': this.reservation.payment?.pie?.cuotas?.toString(),
'payment_credit': this.reservation.payment?.credito?.valor?.toString(),
'promotions[]': this.reservation.promotions?.map(p => p.id.toString()),
'units[]': this.reservation.units?.map(u => u.unit.id.toString()),
'units_value[]': this.reservation.units?.map(u => u.value.toString()),
}
Object.entries(fieldMap).forEach(([key, value]) => {
if (key.includes('[]') && body.has(`${this.prefix}_${key}`)) {
const values = body.getAll(`${this.prefix}_${key}`)
if (values.length !== value.length) {
return
}
const diff = values.some(v => !value.includes(v))
if (diff.length > 0) {
return
}
if (JSON.stringify(values.sort()) !== JSON.stringify(value.sort())) {
return
}
body.delete(`${this.prefix}_${key}`)
return
}
if (body.has(`${this.prefix}_${key}`) && value === body.get(`${this.prefix}_${key}`)) {
body.delete(`${this.prefix}_${key}`)
}
})
return body
}
show() {
this.reset()
this.components.$modal.modal('show')
}
setup() {
this.components.$modal = $(`#${this.ids.modal}`)
this.components.form = document.getElementById(this.ids.form)
this.components.$date = $(`#${this.ids.date}`)
this.components.rut = document.getElementById(this.ids.rut)
this.components.digit = document.getElementById(this.ids.digit)
this.components.$birthdate = $(`#${this.ids.birthdate}`)
this.components.$comuna = $(`#${this.ids.comuna}`)
this.components.$region = $(`#${this.ids.region}`)
this.components.$broker = $(`#${this.ids.broker}`)
this.components.projects = document.getElementById(this.ids.projects)
this.components.project_name = document.getElementById(this.ids.project_name)
this.components.units.parent = this
const cdo = structuredClone(calendar_date_options)
cdo['initialDate'] = new Date()
cdo['maxDate'] = new Date()
this.components.$date.calendar(cdo)
const rutInput = this.components.rut.querySelector('input')
rutInput.addEventListener('input', event => {
const value = event.currentTarget.value.replace(/\D/g, '')
if (value.length <= 3) {
return
}
this.components.digit.textContent = Rut.digitoVerificador(value)
})
rutInput.addEventListener('blur', event => {
const value = event.currentTarget.value.replace(/\D/g, '')
if (value.length <= 3) {
return
}
event.currentTarget.value = Rut.format(value)
this.get.user(value).then(user => {
this.fill.user(user)
})
})
const cdo2 = structuredClone(cdo)
cdo2['initialDate'].setFullYear(cdo2['initialDate'].getFullYear() - 18)
cdo2['maxDate'].setFullYear(cdo2['maxDate'].getFullYear() - 18)
this.components.$birthdate.calendar(cdo2)
this.components.$region.dropdown({
fireOnInit: true,
onChange: this.watch.region
})
this.components.$region.dropdown('set selected', 13)
this.components.$comuna.dropdown()
this.components.$broker.dropdown()
this.components.$loader = $(`#${this.ids.loader}`)
}
}
</script>
@endpush

View File

@ -0,0 +1,131 @@
@push('page_scripts')
<script>
class ModalPayments {
prefix
ids = {
pie: {
checkbox: '',
value: '',
installments: ''
},
credit: {
checkbox: '',
value: ''
}
}
components = {
pie: {
$checkbox: null,
value: null,
installments: null
},
credit: {
$checkbox: null,
value: null
}
}
data = {
pie: {
value: '',
installments: ''
},
credit: {
value: ''
}
}
constructor(prefix) {
this.prefix = prefix
this.ids = {
pie: {
checkbox: `${this.prefix}_payment_has_pie`,
value: `${this.prefix}_payment_pie`,
installments: `${this.prefix}_payment_cuotas`
},
credit: {
checkbox: `${this.prefix}_payment_has_credit`,
value: `${this.prefix}_payment_credit`
}
}
this.setup()
}
reset() {
this.components.pie.$checkbox.prop('checked', false)
this.components.pie.value.value = ''
this.components.pie.installments.value = ''
this.components.credit.$checkbox.prop('checked', false)
this.components.credit.value.value = ''
}
show = {
pie: () => {
this.components.pie.value.style.display = this.data.pie.value
this.components.pie.installments.style.display = this.data.pie.installments
},
credit: () => {
this.components.credit.value.style.display = this.data.credit.value
}
}
hide = {
pie: () => {
this.components.pie.value.style.display = 'none'
this.components.pie.installments.style.display = 'none'
},
credit: () => {
this.components.credit.value.style.display = 'none'
},
all: () => {
this.hide.pie()
this.hide.credit()
}
}
fill(payment) {
if ('pie' in payment && payment.pie !== null) {
this.components.pie.$checkbox.prop('checked', true)
this.show.pie()
this.components.pie.value.querySelector('input').value = payment.pie.valor
this.components.pie.installments.querySelector('input').value = payment.pie.cuotas
}
if ('credito' in payment && payment.credito !== null) {
this.components.credit.$checkbox.prop('checked', true)
this.show.credit()
this.components.credit.value.querySelector('input').value = payment.credito.valor
}
/*if ('subsidio' in payment && payment.subsidio !== null) {
this.components.subsidio.$checkbox.prop('checked', true)
this.components.subsidio.subsidy.value.value = payment.subsidio.subsidio.valor
this.components.subsidio.savings.value.value = payment.subsidio.ahorro.valor
this.show.subsidio()
}*/
}
setup() {
this.components.pie.$checkbox = $(`#${this.ids.pie.checkbox}`)
this.components.pie.value = document.getElementById(this.ids.pie.value)
this.components.pie.installments = document.getElementById(this.ids.pie.installments)
this.components.credit.$checkbox = $(`#${this.ids.credit.checkbox}`)
this.components.credit.value = document.getElementById(this.ids.credit.value)
this.components.pie.$checkbox.checkbox()
this.components.pie.$checkbox.change(changeEvent => {
if (this.components.pie.$checkbox.is(':checked')) {
this.show.pie()
return
}
this.hide.pie()
})
this.components.credit.$checkbox.checkbox()
this.components.credit.$checkbox.change(changeEvent => {
if (this.components.credit.$checkbox.is(':checked')) {
this.show.credit()
return
}
this.hide.credit()
})
this.data.pie.value = this.components.pie.value.style.display
this.data.pie.installments = this.components.pie.installments.style.display
this.data.credit.value = this.components.credit.value.style.display
this.hide.all()
}
}
</script>
@endpush

View File

@ -0,0 +1,117 @@
@push('page_scripts')
<script>
class ModalPromotions {
prefix
ids = {
button: '',
elements: ''
}
data = {
promotions: [],
selected: []
}
components = {
button: null,
promotions: null,
}
display = {
button: ''
}
constructor(prefix) {
this.prefix = prefix
this.ids = {
button: `${prefix}_promotion`,
elements: `${prefix}_promotions`
}
this.setup()
}
add() {
const idx = Math.max(this.data.selected.length, 0, Math.max(...this.data.selected) + 1)
this.data.selected.push(idx)
this.draw.promotions()
return idx
}
reset() {
this.data.selected = []
this.draw.promotions()
}
remove(idx) {
this.data.selected = this.data.selected.filter(promotion => promotion !== idx)
this.draw.promotions()
}
draw = {
promotion: idx => {
const promotions = this.data.promotions.map(promotion => {
return `<div class="item" data-value="${promotion.value}">${promotion.name}</div>`
})
return [
`<div class="fields promotion" data-id="${idx}">`,
'<div class="three wide field">',
'<label>Promoción</label>',
`<div class="ui search selection dropdown">`,
`<input type="hidden" name="${this.prefix}_promotions[]" />`,
'<i class="dropdown icon"></i>',
'<div class="default text">Promoción</div>',
`<div class="menu">${promotions.join('')}</div>`,
'</div>',
'</div>',
'<div class="two wide field">',
'<label></label>',
`<button class="ui red tiny icon button remove_${this.prefix}_promotion" type="button" data-id="${idx}"><i class="trash icon"></i></button>`,
'</div>',
'</div>'
].join('')
},
promotions: () => {
if (this.data.promotions.length === 0) {
this.components.button.parentElement.style.display = 'none'
this.components.promotions.innerHTML = ''
return
}
this.components.button.parentElement.style.display = this.display.button
if (this.data.selected.length === 0) {
return
}
this.components.promotions.innerHTML = this.data.selected.map(idx => {
return this.draw.promotion(idx)
}).join('')
this.components.promotions.querySelectorAll('.dropdown').forEach(dropdown => {
$(dropdown).dropdown()
})
this.components.promotions.querySelectorAll(`.remove_${this.prefix}_promotion`).forEach(button => {
button.addEventListener('click', () => {
const idx = Number(button.dataset.id)
this.remove(idx)
})
})
}
}
async fill(promotions) {
if (reservations.locks.promotions !== null) {
await reservations.locks.promotions
}
const idxs = []
promotions.forEach(promotion => {
const idx = this.add()
idxs.push(idx)
})
let i = 0
promotions.forEach(promotion => {
const idx = idxs[i ++]
const dropdown = $(this.components.promotions.querySelector(`div.promotion[data-id="${idx}"]`).querySelector('div.dropdown'))
dropdown.dropdown('set selected', promotion.id)
})
}
setup() {
this.components.button = document.getElementById(this.ids.button)
this.components.promotions = document.getElementById(this.ids.elements)
this.components.button.addEventListener('click', () => {
this.add()
})
this.display.button = this.components.button.parentElement.style.display
this.draw.promotions()
}
}
</script>
@endpush

View File

@ -0,0 +1,189 @@
@push('page_scripts')
<script>
class ModalUnits {
prefix
parent = null
ids = {
buttons_holder: '',
units: ''
}
data = {
button_map: {},
types: {},
units: [],
}
components = {
buttons_holder: null,
units: null,
}
constructor({parent, prefix}) {
this.prefix = prefix
this.parent = parent
this.ids = {
buttons_holder: `${this.prefix}_unit_buttons`,
units: `${this.prefix}_units`
}
this.data.button_map = {
'departamento': 'building',
'estacionamiento': 'car',
'bodega': 'warehouse',
'terraza': 'vector square'
}
this.setup()
}
get promotions() {
return this.parent.data.promotions[this.parent.data.current_project]
}
draw = {
button: type => {
return [
'<div class="field">',
`<button class="ui icon button" type="button" data-type="${type}" title="${type.charAt(0).toUpperCase() + type.slice(1)}">`,
'<i class="plus icon"></i>',
`<i class="${this.data.button_map[type]} icon"></i>`,
'</button>',
'</div>'
].join('')
},
buttons: () => {
const keys = Object.keys(this.data.types)
if (keys.length === 0) {
return
}
this.components.buttons_holder.innerHTML = keys.map(type => {
return this.draw.button(type)
}).join('')
this.components.buttons_holder.querySelectorAll('.button').forEach(button => {
button.addEventListener('click', () => {
const type = button.dataset.type
this.add(type)
})
})
},
units: () => {
if (this.data.units.length === 0) {
this.components.units.innerHTML = ''
return
}
this.components.units.innerHTML = this.data.units.map(unit => {
return this.draw.unit(unit)
}).join('')
this.components.units.querySelectorAll(`.dropdown.${this.prefix}_units`).forEach(dropdown => {
$(dropdown).dropdown({
onChange: (value, text, $selectedItem) => {
const unitPromotions = this.promotions.filter(promotion => promotion.units.length > 0)
const promotions = unitPromotions.filter(promotion => promotion.units.filter(unit => unit.id === parseInt(value)).length > 0)
$selectedItem.parent().parent().parent().parent().find(`.${this.prefix}_promotions_unit`)
.dropdown('change values', promotions.map(promotion => {
return {
value: promotion.id,
name: promotion.description,
text: promotion.description,
}
}))
}
})
})
this.components.units.querySelectorAll(`.dropdown.${this.prefix}_promotions_unit`).forEach(dropdown => {
$(dropdown).dropdown()
})
this.components.units.querySelectorAll('.remove_unit').forEach(button => {
button.addEventListener('click', () => {
const idx = Number(button.dataset.id)
this.remove(idx)
})
})
},
unit: unit => {
let promotions = ''
if (unit.type === 'departamento') {
if (this.promotions.filter(promotion => promotion.units.length > 0).length > 0) {
promotions = [
'<div class="three wide field">',
'<label>Promociones</label>',
`<div class="ui multiple search selection dropdown ${this.prefix}_promotions_unit">`,
`<input type="hidden" name="${this.prefix}_units_promotions[]" />`,
'<i class="dropdown icon"></i>',
'<div class="default text">Promociones</div>',
'<div class="menu">',
'</div>',
'</div>',
'</div>'
].join('')
}
}
const type = unit.type.charAt(0).toUpperCase() + unit.type.slice(1)
return [
`<div class="fields unit" data-id="${unit.idx}">`,
'<div class="four wide field">',
`<label>${type}</label>`,
`<div class="ui search selection dropdown ${this.prefix}_units">`,
`<input type="hidden" name="${this.prefix}_units[]" />`,
'<i class="dropdown icon"></i>',
`<div class="default text">${type}</div>`,
'<div class="menu">',
this.data.types[unit.type].map(u => {
return `<div class="item" data-value="${u.value}">${u.name}</div>`
}).join(''),
'</div>',
'</div>',
'</div>',
'<div class="three wide field">',
'<label>Valor</label>',
'<div class="ui right labeled input">',
`<input type="text" name="${this.prefix}_units_value[]" placeholder="Valor" />`,
'<div class="ui basic label">UF</div>',
'</div>',
'</div>',
promotions,
'<div class="field">',
'<label></label>',
`<button class="ui red tiny icon button remove_unit" type="button" data-id="${unit.idx}"><i class="trash icon"></i></button>`,
'</div>',
'</div>',
].join('')
}
}
reset() {
this.data.units = []
this.components.units.innerHTML = ''
this.draw.units()
}
add(type) {
const idx = Math.max(this.data.units.length, 0, Math.max(...this.data.units.map(unit => unit.idx)) + 1)
this.data.units.push({idx, type})
this.draw.units()
return idx
}
remove(idx) {
this.data.units = this.data.units.filter(unit => unit.idx !== idx)
this.draw.units()
}
async fill(unitValues) {
if (reservations.locks.units !== null) {
await reservations.locks.units
}
const idxs = []
unitValues.forEach(unitValue => {
const type = unitValue.unit.proyecto_tipo_unidad.tipo_unidad.descripcion
const idx = this.add(type)
idxs.push(idx)
})
let i = 0
unitValues.forEach(unitValue => {
const idx = idxs[i ++]
const field = this.components.units.querySelector(`div.unit[data-id="${idx}"]`)
$(field.querySelector('div.dropdown.edit_units')).dropdown('set selected', unitValue.unit.id)
field.querySelector('div.input').querySelector('input').value = unitValue.value
})
}
setup() {
this.components.buttons_holder = document.getElementById(this.ids.buttons_holder)
this.components.units = document.getElementById(this.ids.units)
this.draw.buttons()
}
}
</script>
@endpush

View File

@ -0,0 +1,182 @@
@include('ventas.reservations.modal.common.modal', ['prefix' => 'edit', 'modalTitle' => 'Editar Reserva', 'okText' => 'Guardar Cambios'])
@push('page_scripts')
<script>
class EditModalPromotions extends ModalPromotions {
constructor() {
super('edit');
}
}
class EditModalUnits extends ModalUnits {
constructor(parent) {
super({prefix: 'edit', parent})
}
}
class EditModalPayments extends ModalPayments {
constructor() {
super('edit');
}
}
class EditReservationModal extends ReservationModal {
reservation = null
constructor(projects_id) {
super({projects_id, prefix: 'edit', components: {
promotions: new EditModalPromotions(),
units: new EditModalUnits(),
payments: new EditModalPayments()
}});
}
show({type, reservation_id}) {
const reservation = reservations.components.reservations[type].reservations.find(r => r.id === parseInt(reservation_id))
super.show()
this.fillReservation(reservation)
}
reset() {
super.reset()
this.reservation = null
}
async fillReservation(reservation) {
this.reservation = reservation
this.components.form.querySelector(`#${this.prefix}_reservation_id`).value = reservation.id
const date = new Date(reservation.date)
date.setDate(date.getDate() + 1)
this.components.$date.calendar('set date', date)
this.components.rut.querySelector(`input[name="${this.prefix}_buyer_rut"]`).value = Rut.format(reservation.buyer.rut) || ''
this.components.digit.textContent = reservation.buyer.digito || ''
this.components.form.querySelector(`[name="${this.prefix}_buyer_name"]`).value = reservation.buyer.nombres || ''
this.components.form.querySelector(`[name="${this.prefix}_buyer_last_name"]`).value = reservation.buyer.apellidoPaterno || ''
this.components.form.querySelector(`[name="${this.prefix}_buyer_last_name2"]`).value = reservation.buyer.apellidoMaterno || ''
if ('datos' in reservation.buyer && reservation.buyer.datos !== null) {
if ('direccion' in reservation.buyer.datos && reservation.buyer.datos.direccion !== null) {
this.components.form.querySelector(`[name="${this.prefix}_buyer_address_street"]`).value = reservation.buyer.datos.direccion.calle
this.components.form.querySelector(`[name="${this.prefix}_buyer_address_number"]`).value = reservation.buyer.datos.direccion.numero
this.components.form.querySelector(`[name="${this.prefix}_buyer_address_extra"]`).value = reservation.buyer.datos.direccion.extra
this.components.$region.dropdown('set selected', reservation.buyer.datos.direccion.comuna.provincia.region.id)
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const waitComuna = async () => {
await delay(500)
this.components.$comuna.dropdown('set selected', reservation.buyer.datos.direccion.comuna.id)
}
await waitComuna()
}
this.components.form.querySelector(`[name="${this.prefix}_buyer_phone"]`).value = reservation.buyer.datos.telefono || ''
if ('email' in reservation.buyer.datos && reservation.buyer.datos.email !== null && reservation.buyer.datos.email.includes('@') && reservation.buyer.datos.email.length > 1) {
const parts = reservation.buyer.datos.email.split('@')
this.components.form.querySelector(`[name="${this.prefix}_buyer_email_name"]`).value = parts[0]
this.components.form.querySelector(`[name="${this.prefix}_buyer_email_domain"]`).value = parts[1]
}
this.components.form.querySelector(`[name="${this.prefix}_buyer_marital_status"]`).value = reservation.buyer.datos.estadoCivil || ''
this.components.form.querySelector(`[name="${this.prefix}_buyer_profession"]`).value = reservation.buyer.datos.ocupacion || ''
if ('fechaNacimiento' in reservation.buyer.datos) {
const birthdate = new Date(reservation.buyer.datos.fechaNacimiento)
birthdate.setDate(birthdate.getDate() + 1)
this.components.$birthdate.calendar('set date', birthdate)
}
}
if (reservation.broker !== null) {
if (reservations.locks.brokers === null) {
await reservations.locks.brokers
}
this.components.$broker.dropdown('set selected', reservation.broker.rut)
}
if (reservation.promotions.length > 0) {
this.components.promotions.fill(reservation.promotions)
}
if (reservation.units.length > 0) {
this.components.units.fill(reservation.units)
}
if ('payment' in reservation && reservation.payment !== null) {
this.components.payments.fill(reservation.payment)
}
}
edit() {
const id = this.components.form.querySelector(`#${this.prefix}_reservation_id`).value
const url = `/api/ventas/reservation/${id}/edit`
const form = document.getElementById(this.ids.form)
let body = new FormData(form)
body.delete(`${this.prefix}_reservation_id`)
body.delete(`${this.prefix}_project_id`)
const date = this.components.$date.calendar('get date')
body.set(`${this.prefix}_date`, [
date.getFullYear(),
(date.getMonth() + 1).toString().padStart(2, '0'),
date.getDate().toString().padStart(2, '0')].join('-'))
body.set(`${this.prefix}_buyer_rut`, Rut.clean(this.components.rut.querySelector('input').value))
body.set(`${this.prefix}_buyer_digit`, this.components.digit.textContent)
body.set(`${this.prefix}_buyer_address_comuna_id`, this.components.$comuna.dropdown('get value'))
let email = form.querySelector(`input[name='${this.prefix}_buyer_email_name']`).value + '@' + form.querySelector(`input[name='${this.prefix}_buyer_email_domain']`).value
if (email !== '@') {
body.set(`${this.prefix}_buyer_email`, email)
}
const birthdate = this.components.$birthdate.calendar('get date')
body.set(`${this.prefix}_buyer_birthdate`, [
birthdate.getFullYear(),
(birthdate.getMonth() + 1).toString().padStart(2, '0'),
birthdate.getDate().toString().padStart(2, '0')].join('-'))
body.set(`${this.prefix}_broker_rut`, Rut.clean(this.components.$broker.dropdown('get value')))
if (!this.components.payments.components.pie.$checkbox.is(':checked')) {
body.delete(`${this.prefix}_payment_pie`)
body.delete(`${this.prefix}_payment_cuotas`)
}
body.delete(`${this.prefix}_payment_has_pie`)
if (!this.components.payments.components.credit.$checkbox.is(':checked')) {
body.delete(`${this.prefix}_payment_credit`)
}
body.delete(`${this.prefix}_payment_has_credit`)
body.delete('comuna')
body.delete('region')
body.delete('broker')
body.delete(`${this.prefix}_broker`)
body.delete('birthdate')
body.delete(`${this.prefix}_buyer_email_name`)
body.delete(`${this.prefix}_buyer_email_domain`)
const keys = body.keys().toArray()
keys.forEach(key => {
if (body.get(key) === '' || body.get(key) === null) {
body.delete(key)
}
})
body = this.validateBody(body)
if (body.keys().toArray().length === 0) {
alert('No hay cambios.')
return
}
const method = 'post'
return APIClient.fetch(url, {method, body}).then(response => response.json()).then(json => {
if (json.success) {
window.location.reload()
}
})
}
setup() {
super.setup()
this.components.$modal.modal({
onApprove: () => {
this.edit()
}
})
this.components.form.addEventListener('submit', event => {
event.preventDefault()
this.edit()
return false
})
const idInput = document.createElement('input')
idInput.type = 'hidden'
idInput.id = `${this.prefix}_reservation_id`
idInput.name = `${this.prefix}_reservation_id`
this.components.form.appendChild(idInput)
}
}
</script>
@endpush

View File

@ -18,6 +18,14 @@ return [
$urls['assets'],
'images'
]);
$urls['scripts'] = implode('/', [
$urls['assets'],
'js'
]);
$urls['styles'] = implode('/', [
$urls['assets'],
'css'
]);
return (object) $urls;
},
'apiUrls' => function(ContainerInterface $container) {

View File

@ -15,21 +15,24 @@ return [
);
},
Incoviba\Service\Money::class => function(ContainerInterface $container) {
$mindicador = new Incoviba\Service\Money\MiIndicador(new GuzzleHttp\Client([
$mindicador = new Incoviba\Service\Money\MiIndicador($container->get(Psr\Log\LoggerInterface::class), new GuzzleHttp\Client([
'base_uri' => 'https://mindicador.cl/api/',
'headers' => ['Accept' => 'application/json']
]));
$ine = new Incoviba\Service\Money\Ine(new GuzzleHttp\Client([
$ine = new Incoviba\Service\Money\Ine($container->get(Psr\Log\LoggerInterface::class), new GuzzleHttp\Client([
'base_uri' => 'https://api-calculadora.ine.cl/ServiciosCalculadoraVariacion'
]));
$sii = new Incoviba\Service\Money\SII(new GuzzleHttp\Client([
$sii = new Incoviba\Service\Money\SII($container->get(Psr\Log\LoggerInterface::class), new GuzzleHttp\Client([
'base_uri' => 'https://www.sii.cl/valores_y_fechas/'
]), $container->get(Incoviba\Repository\UF::class));
return (new Incoviba\Service\Money($container->get(Psr\Log\LoggerInterface::class)))
->register('uf', $mindicador)
->register('uf', $sii)
->register('usd', $mindicador)
->register('ipc', $ine);
$findic = new Incoviba\Service\Money\Findic($container->get(Psr\Log\LoggerInterface::class), new GuzzleHttp\Client([
'base_uri' => 'https://findic.cl/api/'
]));
return new Incoviba\Service\Money($container->get(Psr\Log\LoggerInterface::class))
->register($findic)
->register($sii)
->register($ine)
->register($mindicador);
},
Predis\ClientInterface::class => function(ContainerInterface $container) {
$options = [
@ -46,7 +49,7 @@ return [
return new Predis\Client($options);
},
Incoviba\Service\Contabilidad\Cartola::class => function(ContainerInterface $container) {
return (new Incoviba\Service\Contabilidad\Cartola(
return new Incoviba\Service\Contabilidad\Cartola(
$container->get(Psr\Log\LoggerInterface::class),
$container->get(Psr\Http\Message\StreamFactoryInterface::class),
$container->get(Incoviba\Common\Define\Contabilidad\Exporter::class),
@ -55,7 +58,7 @@ return [
$container->get(Incoviba\Repository\Contabilidad\Movimiento::class),
$container->get(Incoviba\Service\Contabilidad\Movimiento::class),
$container->get(Incoviba\Repository\Contabilidad\Cartola::class)
))
)
->register('security', $container->get(Incoviba\Service\Contabilidad\Cartola\Security::class))
->register('itau', $container->get(Incoviba\Service\Contabilidad\Cartola\Itau::class))
->register('santander', $container->get(Incoviba\Service\Contabilidad\Cartola\Santander::class))
@ -89,11 +92,10 @@ return [
$container->get('nubox')->get('url'));
},
Incoviba\Service\Informe::class => function(ContainerInterface $container) {
return (new Incoviba\Service\Informe(
return new Incoviba\Service\Informe(
$container->get(Psr\Log\LoggerInterface::class),
$container->get('folders')->get('informes'))
)
->register('xlsx', Incoviba\Service\Informe\Excel::class);
->register('xlsx', Incoviba\Service\Informe\Excel::class);
},
Incoviba\Service\Contabilidad\Informe\Tesoreria\Output\Excel::class => function(ContainerInterface $container) {
return new Incoviba\Service\Contabilidad\Informe\Tesoreria\Output\Excel(
@ -104,15 +106,15 @@ return [
);
},
Incoviba\Service\Contabilidad\Cartola\Santander::class => function(ContainerInterface $container) {
return (new Incoviba\Service\Contabilidad\Cartola\Santander(
return new Incoviba\Service\Contabilidad\Cartola\Santander(
$container->get(Psr\Log\LoggerInterface::class),
))
)
->registerSub($container->get(Incoviba\Service\Contabilidad\Cartola\Santander\OfficeBanking::class));
},
Incoviba\Service\Contabilidad\Cartola\BCI::class => function(ContainerInterface $container) {
return (new Incoviba\Service\Contabilidad\Cartola\BCI(
return new Incoviba\Service\Contabilidad\Cartola\BCI(
$container->get(Psr\Log\LoggerInterface::class),
))
)
->registerSub($container->get(Incoviba\Service\Contabilidad\Cartola\BCI\Mes::class));
},
'TokuClient' => function(ContainerInterface $container) {

View File

@ -2,6 +2,7 @@
namespace Incoviba\Controller\API\Contabilidad;
use DateTimeImmutable;
use Incoviba\Exception\ServiceAction\Read;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Incoviba\Common\Ideal\Controller;
@ -117,24 +118,24 @@ class Cartolas extends Controller
UPLOAD_ERR_CANT_WRITE => 'No se pudo escribir el archivo',
UPLOAD_ERR_EXTENSION => 'Una extensión de PHP detuvo la subida del archivo'
];
if (is_array($files['file'])) {
foreach ($files['file'] as $i => $file) {
if ($file->getError() !== UPLOAD_ERR_OK) {
$output['errors'] []= ['filename' => $file->getClientFilename(), 'error' => $errors[$file->getError()]];
continue;
}
try {
$output['movimientos'] = array_merge($output['movimientos'], $cartolaService->import($body['cuenta_id'][$i], $file));
} catch (EmptyResult) {}
}
} else {
$file = $files['file'];
if (!is_array($files['file'])) {
$files['file'] = [$files['file']];
$body['cuenta_id'] = [$body['cuenta_id']];
}
foreach ($files['file'] as $i => $file) {
if ($file->getError() !== UPLOAD_ERR_OK) {
$output['errors'][] = ['filename' => $file->getClientFilename(), 'error' => $errors[$file->getError()]];
} else {
try {
$output['movimientos'] = $cartolaService->import($body['cuenta_id'], $file);
} catch (EmptyResult) {}
$output['errors'] []= ['filename' => $file->getClientFilename(), 'error' => $errors[$file->getError()]];
continue;
}
if (empty($body['cuenta_id'])) {
continue;
}
try {
$output['movimientos'] = array_merge($output['movimientos'], $cartolaService->import($body['cuenta_id'][$i], $file));
} catch (Read $exception) {
$output['errors'] []= ['filename' => $file->getClientFilename(),
'error' => ['message' => $exception->getMessage(), 'file' => $exception->getFile(),
'line' => $exception->getLine()]];
}
}
return $this->withJson($response, $output);

View File

@ -88,8 +88,8 @@ class Movimientos extends Ideal\Controller
$sociedades_ruts = $input['sociedades_ruts'];
foreach ($sociedades_ruts as $sociedadRut) {
try {
$movimientos = $movimientoService->getAmountBySociedadAndMes($sociedadRut, new DateTimeImmutable($input['mes']), $input['amount'] ?? null);
$output['movimientos'] = array_merge($output['movimientos'], $this->movimientosToArray($movimientos));
$movimientos = $movimientoService->getAmountBySociedadAndMes($sociedadRut, new DateTimeImmutable($input['mes']), $input['amount'] ?? null);
$output['movimientos'] = array_merge($output['movimientos'], $this->movimientosToArray($movimientos));
} catch (EmptyResult) {}
}
return $this->withJson($response, $output);

View File

@ -53,7 +53,7 @@ class Money
}
try {
$this->data[$provider] = (array) $this->fetchRedis($redisService, $redisKey);
if (!isset($this->data[$provider][$date->format('Y-m-d')])) {
if (!isset($this->data[$provider][$date->format('Y-m-d')]) or $this->data[$provider][$date->format('Y-m-d')] === 0) {
throw new EmptyRedis($redisKey);
}
} catch (EmptyRedis) {

View File

@ -4,8 +4,11 @@ namespace Incoviba\Controller\API\Ventas;
use DateTimeImmutable;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Controller\API\withJson;
use Incoviba\Exception\ServiceAction\Read;
use Incoviba\Exception\ServiceAction\Update;
use Incoviba\Repository;
use Incoviba\Service;
@ -42,7 +45,7 @@ class Escrituras
try {
$escrituraService->edit($venta_id, $body);
$output['success'] = true;
} catch (EmptyResult) {}
} catch (Read|Update|EmptyResult) {}
return $this->withJson($response, $output);
}
}

View File

@ -5,14 +5,14 @@ use DateTime;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Incoviba\Common\Ideal\Controller;
use Incoviba\Common\Implement\Exception\{EmptyRedis,EmptyResult};
use Psr\Log\LoggerInterface;
use Incoviba\Controller\API\{withJson,emptyBody};
use Incoviba\Controller\withRedis;
use Incoviba\Exception\ServiceAction\Create;
use Incoviba\Service;
class Precios
class Precios extends Controller
{
use withJson, emptyBody, withRedis;
@ -56,12 +56,16 @@ class Precios
}
public function import(ServerRequestInterface $request, ResponseInterface $response,
LoggerInterface $logger,
Service\Venta\Precio $precioService): ResponseInterface
{
$body = $request->getParsedBody();
$projectId = $body['project_id'];
$date = $body['date'];
if (array_key_exists('date', $body)) {
$date = $body['date'];
$date = DateTime::createFromFormat('Y-m-d', $date);
} else {
$date = new DateTime();
}
$file = $request->getUploadedFiles()['file'];
$output = [
'input' => $body,
@ -69,13 +73,12 @@ class Precios
'precios' => [],
'status' => false
];
$date = DateTime::createFromFormat('Y-m-d', $date);
try {
$output['precios'] = $precioService->import($projectId, $date, $file);
$output['total'] = count($output['precios']);
$output['status'] = true;
} catch (Create | Exception $exception) {
$logger->warning($exception);
$this->logger->warning($exception);
}
return $this->withJson($response, $output);
}

View File

@ -108,7 +108,7 @@ class Reservations
$output = [
'input' => $input,
'reservation_id' => $reservation_id,
'reservations' => null,
'reservation' => null,
'success' => false,
];

View File

@ -1,6 +1,10 @@
<?php
namespace Incoviba\Controller;
use Exception;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Alias\View;
use Incoviba\Common\Implement\Exception\EmptyRedis;
use Incoviba\Common\Implement\Exception\EmptyResult;
@ -10,8 +14,6 @@ use Incoviba\Exception\ServiceAction\Update;
use Incoviba\Model;
use Incoviba\Repository;
use Incoviba\Service;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class Ventas
{
@ -91,14 +93,78 @@ class Ventas
return $view->render($response, 'ventas.pies.edit', compact('venta'));
}
public function add(ServerRequestInterface $request, ResponseInterface $response, View $view,
Repository\Region $regionRepository, Repository\Proyecto $proyectoRepository): ResponseInterface
Repository\Region $regionRepository, Repository\Proyecto $proyectoRepository,
LoggerInterface $logger,
?Service\Venta\Reservation $reservaService = null): ResponseInterface
{
$regiones = $regionRepository->fetchAll();
usort($regiones, function(Model\Region $a, Model\Region $b) {
return $a->numeracion - $b->numeracion;
});
$proyectos = $proyectoRepository->fetchAllActive();
return $view->render($response, 'ventas.add', compact('regiones', 'proyectos'));
$viewData = [
'regiones' => $regiones,
'proyectos' => $proyectos,
'from_reservation' => false
];
// Check if this is a conversion from a reservation
if ($request->getMethod() === 'POST') {
$data = $request->getParsedBody();
if (isset($data['from_reservation']) && $data['from_reservation'] === 'true' && !empty($data['reservation_id'])) {
try {
$reservation = $reservaService->get((int) $data['reservation_id']);
$viewData['from_reservation'] = true;
$viewData['reservation_id'] = $reservation->id;
$viewData['date'] = $reservation->date;
$viewData['comments'] = explode("\n", $reservation->comments);
$viewData['propietario'] = [
'rut' => $reservation->buyer->rut,
'digito' => $reservation->buyer->digito,
'full' => $reservation->buyer->rutCompleto(),
'email' => $reservation->buyer->datos()?->email,
'telefono' => $reservation->buyer->datos()?->telefono,
'comuna_id' => $reservation->buyer->datos()?->direccion?->comuna?->id
];
// Add property data
$viewData['proyecto_id'] = $reservation->project->id;
$viewData['unidades'] = [];
foreach ($reservation->units as $unitValue) {
$type = $unitValue->unit->proyectoTipoUnidad->tipoUnidad->descripcion;
if (!array_key_exists($type, $viewData['unidades'])) {
$viewData['unidades'][$type] = [];
}
$viewData['unidades'][$type] []= $unitValue->unit->id;
}
$viewData['valor'] = $reservation->offer();
if ($reservation->payment !== null) {
$viewData['forma_pago'] = [];
if ($reservation->payment->pie !== null) {
$viewData['forma_pago']['pie'] = [
'valor' => $reservation->payment->pie->valor,
'cuotas' => $reservation->payment->pie->cuotas,
];
}
if ($reservation->payment->credito !== null) {
$viewData['forma_pago']['credito'] = $reservation->payment->credito->valor;
}
}
} catch (Exception $exception) {
// Log error or handle as needed
$logger->error('Error loading reservation data', ['exception' => $exception]);
}
}
}
return $view->render($response, 'ventas.add', $viewData);
}
public function cuotas(ServerRequestInterface $request, ResponseInterface $response, Service\Venta $ventaService,
Service\Contabilidad\Banco $bancoService,

View File

@ -14,6 +14,7 @@ class Reservation extends Common\Ideal\Model
public array $units = [];
public array $promotions = [];
public ?Model\Proyecto\Broker $broker = null;
public ?Model\Venta\Reservation\Payment $payment = null;
public function offer(): float
{
@ -22,10 +23,13 @@ class Reservation extends Common\Ideal\Model
public function withCommission(): float
{
$base = 0;
foreach ($this->units as $unit) {
foreach ($this->units as &$unit) {
$unitBase = $unit->value;
foreach ($this->promotions as $promotion) {
$base += $promotion->activate()->reverse($unit['unit'], $unit['value'], $this->broker);
$unitBase = $promotion->activate()->reverse($unit->unit, $unitBase, $this->broker);
}
$unit->base = $unitBase;
$base += $unitBase;
}
return $base;
}
@ -128,6 +132,7 @@ class Reservation extends Common\Ideal\Model
'units' => $this->units,
'promotions' => $this->promotions,
'broker' => $this->broker,
'payment' => $this->payment,
'offer' => $this->offer(),
'with_commission' => $this->withCommission(),
'base' => $this->base(),

View File

@ -6,6 +6,9 @@ enum Type: int
case Unit = 1;
case Promotion = 2;
case Broker = 3;
case Advance = 4; // Pie
case Credit = 5; // Credito
case Subsidy = 6; // Subsidio
public function jsonSerialize(): array
{

View File

@ -0,0 +1,23 @@
<?php
namespace Incoviba\Model\Venta\Reservation;
use JsonSerializable;
use Incoviba\Model\Venta\Credito;
use Incoviba\Model\Venta\Pie;
use Incoviba\Model\Venta\Subsidio;
class Payment implements JsonSerializable
{
public ?Pie $pie = null;
public ?Credito $credito = null;
public ?Subsidio $subsidio = null;
public function jsonSerialize(): array
{
return [
'pie' => $this->pie,
'credito' => $this->credito,
'subsidio' => $this->subsidio
];
}
}

View File

@ -54,6 +54,11 @@ class Movimiento extends Ideal\Repository
return $this->update($model, ['cuenta_id', 'fecha', 'glosa', 'documento', 'cargo', 'abono', 'saldo'], $new_data);
}
/**
* @param int $cuenta_id
* @return array
* @throws Implement\Exception\EmptyResult
*/
public function fetchByCuenta(int $cuenta_id): array
{
$query = $this->connection->getQueryBuilder()
@ -97,6 +102,13 @@ class Movimiento extends Ideal\Repository
->where("cuenta_id = ? AND fecha = ? AND SUBSTRING(LOWER(LTRIM(glosa)), 0, {$len}) = SUBSTRING(LOWER(LTRIM(?)), 0, {$len}) AND cargo = ? AND abono = ? AND saldo = ?");
return $this->fetchOne($query, [$cuenta_id, $fecha->format('Y-m-d'), $glosa, $cargo, $abono, $saldo]);
}
/**
* @param int $start
* @param int $amount
* @return array
* @throws Implement\Exception\EmptyResult
*/
public function fetchAmountStartingFrom(int $start, int $amount): array
{
$query = $this->connection->getQueryBuilder()
@ -105,6 +117,14 @@ class Movimiento extends Ideal\Repository
->limit($amount, $start);
return $this->fetchMany($query);
}
/**
* @param int $sociedad_rut
* @param DateTimeInterface $mes
* @param int|null $amount
* @return array
* @throws Implement\Exception\EmptyResult
*/
public function fetchAmountBySociedadAndMes(int $sociedad_rut, DateTimeInterface $mes, ?int $amount): array
{
$query = $this->connection->getQueryBuilder()

View File

@ -28,6 +28,9 @@ class Detalle extends Ideal\Repository
->register('centro_costo_id', (new Implement\Repository\Mapper())
->setProperty('centroCosto')
->setFunction(function(array $data) {
if (empty($data['centro_costo_id'])) {
return null;
}
return $this->centroCostoRepository->fetchById($data['centro_costo_id']);
})
->setDefault(null));

View File

@ -19,10 +19,13 @@ class Proveedor extends Ideal\Repository
public function create(?array $data = null): Model\Inmobiliaria\Proveedor
{
$map = (new Implement\Repository\MapperParser(['rut', 'digito', 'nombre', 'razon']))
$map = new Implement\Repository\MapperParser(['rut', 'digito', 'nombre', 'razon'])
->register('contacto_rut', (new Implement\Repository\Mapper())
->setProperty('contacto')
->setFunction(function($data) {
if ($data['contacto_rut'] === null) {
return null;
}
return $this->personaService->getById($data['contacto_rut']);
})
->setDefault(null));

View File

@ -63,7 +63,7 @@ class Datos extends Ideal\Repository
'persona_rut', 'direccion_id', 'telefono', 'email', 'fecha_nacimiento', 'sexo', 'estado_civil',
'nacionalidad', 'ocupacion'
], [
$model->persona->rut, $model->direccion?->id, $model->telefono, $model->email, $model->fechaNacimiento,
$model->persona->rut, $model->direccion?->id, $model->telefono, $model->email, $model->fechaNacimiento->format('Y-m-d'),
$model->sexo, $model->estadoCivil, $model->nacionalidad, $model->ocupacion
]);
return $model;

View File

@ -3,22 +3,28 @@ namespace Incoviba\Repository\Venta;
use DateTimeInterface;
use DateInterval;
use Incoviba\Exception\ServiceAction\Read;
use PDO;
use PDOException;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Define;
use Incoviba\Common\Implement\Exception\EmptyResult;
use Incoviba\Exception\Model\InvalidState;
use PDO;
use Incoviba\Common;
use Incoviba\Model;
use Incoviba\Repository;
use PDOException;
use Incoviba\Service;
class Reservation extends Common\Ideal\Repository
{
public function __construct(Common\Define\Connection $connection,
protected LoggerInterface $logger,
protected Repository\Proyecto $proyectoRepository,
protected Repository\Persona $personaRepository,
protected Repository\Proyecto\Broker $brokerRepository,
protected Unidad $unitRepository, protected Promotion $promotionRepository)
protected Service\Proyecto\Broker $brokerService,
protected Unidad $unitRepository,
protected Promotion $promotionRepository, protected Pie $advanceRepository,
protected Credito $creditRepository, protected Subsidio $subsidyRepository)
{
parent::__construct($connection);
}
@ -44,6 +50,12 @@ class Reservation extends Common\Ideal\Repository
->register('date', new Common\Implement\Repository\Mapper\DateTime('date'));
return $this->parseData(new Model\Venta\Reservation(), $data, $map);
}
/**
* @param Define\Model $model
* @return Model\Venta\Reservation
* @throws PDOException
*/
public function save(Common\Define\Model $model): Model\Venta\Reservation
{
if (!isset($model->id)) {
@ -85,8 +97,15 @@ class Reservation extends Common\Ideal\Repository
$this->fetchUnits($model, $data_row);
try {
$this->fetchBroker($model, $data_row);
} catch (Common\Implement\Exception\EmptyResult) {}
} catch (EmptyResult $exception) {
$this->logger->notice(implode("::", [__CLASS__, __METHOD__]), ['data' => $data_row, 'exception' => $exception]);
}
$this->fetchPromotions($model, $data_row);
try {
$this->fetchPayment($model, $data_row);
} catch (EmptyResult $exception) {
$this->logger->notice(implode("::", [__CLASS__, __METHOD__]), ['data' => $data_row, 'exception' => $exception]);
}
return $model;
}
@ -239,11 +258,20 @@ class Reservation extends Common\Ideal\Repository
$exceptions[] = new Common\Implement\Exception\EmptyResult('No cancelled reservations', $exception);
}
if (count($reservations) === 0) {
throw new Common\Implement\Exception\EmptyResult('No rejected nor cancelled reservations', last($exceptions));
$exception = null;
if (count($exceptions) > 0) {
$exception = last($exceptions);
}
throw new Common\Implement\Exception\EmptyResult('No rejected nor cancelled reservations', $exception);
}
return $reservations;
}
/**
* @param Model\Venta\Reservation $reservation
* @return void
* @throws PDOException
*/
protected function saveUnits(Model\Venta\Reservation $reservation): void
{
if (empty($reservation->units)) {
@ -286,6 +314,12 @@ class Reservation extends Common\Ideal\Repository
Model\Venta\Reservation\Detail\Type::Unit->value,
array_map(fn($unit) => $unit->unit->id, $reservation->units));
}
/**
* @param Model\Venta\Reservation $reservation
* @return void
* @throws PDOException
*/
protected function saveBroker(Model\Venta\Reservation &$reservation): void
{
if ($reservation->broker === null) {
@ -306,9 +340,9 @@ class Reservation extends Common\Ideal\Repository
throw new PDOException();
}
$new_id = $reservation->broker->rut;
$reservation->broker = $this->brokerRepository->fetchById($result['reference_id']);
$reservation->broker = $this->brokerService->fetchById($result['reference_id']);
$this->editBroker($reservation, ['broker_rut' => $new_id]);
} catch (PDOException) {
} catch (EmptyResult | PDOException) {
$queryInsert = $this->connection->getQueryBuilder()
->insert()
->into('reservation_details')
@ -321,6 +355,12 @@ class Reservation extends Common\Ideal\Repository
]);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @return void
* @throws PDOException
*/
protected function savePromotions(Model\Venta\Reservation $reservation): void
{
if (empty($reservation->promotions)) {
@ -362,6 +402,34 @@ class Reservation extends Common\Ideal\Repository
Model\Venta\Reservation\Detail\Type::Promotion->value,
array_map(fn($promotion) => $promotion->id, $reservation->promotions));
}
public function saveDetail(Model\Venta\Reservation $reservation, int $type, int $referenceId, ?float $value = null): void
{
$queryCheck = $this->connection->getQueryBuilder()
->select('1')
->from('reservation_details')
->where('reservation_id = :reservation_id AND type = :type AND reference_id = :reference_id AND value = :value');
$statementCheck = $this->connection->prepare($queryCheck);
$statementCheck->execute([
'reservation_id' => $reservation->id,
'type' => $type,
'reference_id' => $referenceId,
'value' => $value
]);
$result = $statementCheck->fetch(PDO::FETCH_ASSOC);
if ($result === false) {
$queryInsert = $this->connection->getQueryBuilder()
->insert()
->into('reservation_details')
->columns(['reservation_id', 'type', 'reference_id', 'value'])
->values([':reservation_id', ':type', ':reference_id', ':value']);
$this->connection->execute($queryInsert, [
'reservation_id' => $reservation->id,
'type' => $type,
'reference_id' => $referenceId,
'value' => $value
]);
}
}
protected function cleanUpDetails(Model\Venta\Reservation $reservation, int $type_id, array $currentIds): void
{
$queryCheck = $this->connection->getQueryBuilder()
@ -394,6 +462,9 @@ class Reservation extends Common\Ideal\Repository
protected function editUnits(Model\Venta\Reservation &$reservation, array $new_data): void
{
if (!array_key_exists('units', $new_data)) {
return;
}
$querySelect = $this->connection->getQueryBuilder()
->select()
->from('reservation_details')
@ -410,6 +481,7 @@ class Reservation extends Common\Ideal\Repository
->columns(['reservation_id', 'type', 'reference_id', 'value'])
->values([':reservation_id', ':type', ':reference_id', ':value']);
$statementInsert = $this->connection->prepare($queryInsert);
foreach ($new_data['units'] as $unit) {
$idx = $reservation->findUnit($unit['unit_id']);
if ($idx === null) {
@ -447,6 +519,7 @@ class Reservation extends Common\Ideal\Repository
$reservation->units[$idx]['value'] = $unit['value'];
}
}
protected function editBroker(Model\Venta\Reservation &$reservation, array $new_data): void
{
if (!array_key_exists('broker_rut', $new_data) or $new_data['broker_rut'] === $reservation->broker->rut) {
@ -462,11 +535,14 @@ class Reservation extends Common\Ideal\Repository
'type' => Model\Venta\Reservation\Detail\Type::Broker->value,
'broker_rut' => $new_data['broker_rut']
]);
$reservation->broker = $this->brokerRepository->fetchById($new_data['broker_rut']);
} catch (PDOException) {}
$reservation->broker = $this->brokerService->get($new_data['broker_rut']);
} catch (PDOException|Read) {}
}
protected function editPromotions(Model\Venta\Reservation &$reservation, array $new_data): void
{
if (!array_key_exists('promotions', $new_data)) {
return;
}
$querySelect = $this->connection->getQueryBuilder()
->select()
->from('reservation_details')
@ -477,7 +553,7 @@ class Reservation extends Common\Ideal\Repository
->set('value = :value')
->where('reservation_id = :id AND type = "Promotion" AND reference_id = :promotion_id');
$statementUpdate = $this->connection->prepare($queryUpdate);
foreach ($new_data as $promotion_id => $value) {
foreach ($new_data['promotions'] as $promotion_id => $value) {
$idx = array_search($promotion_id, array_column($reservation->promotions, 'id'));
if ($idx === false) {
$reservation->promotions []= $this->promotionRepository->fetchById($promotion_id);
@ -569,14 +645,18 @@ class Reservation extends Common\Ideal\Repository
if ($result === false) {
throw new Common\Implement\Exception\EmptyResult($query);
}
$reservation->broker = $this->brokerRepository->fetchById($result['reference_id']);
} catch (PDOException) {}
$reservation->broker = $this->brokerService->get($result['reference_id']);
} catch (PDOException | Read $exception) {
$this->logger->notice(implode("::", [__CLASS__, __METHOD__]), ['data' => $new_data, 'exception' => $exception]);
}
return $reservation;
}
try {
$reservation->broker = $this->brokerRepository->fetchById($new_data['broker_id']);
} catch (Common\Implement\Exception\EmptyResult) {}
$reservation->broker = $this->brokerService->get($new_data['broker_id']);
} catch (Read $exception) {
$this->logger->notice(implode("::", [__CLASS__, __METHOD__]), ['data' => $new_data, 'exception' => $exception]);
}
return $reservation;
}
protected function fetchPromotions(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation
@ -639,4 +719,49 @@ class Reservation extends Common\Ideal\Repository
$reservation->broker = null;
} catch (PDOException) {}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $new_data
* @return Model\Venta\Reservation
* @throws EmptyResult
*/
protected function fetchPayment(Model\Venta\Reservation &$reservation, array $new_data): Model\Venta\Reservation
{
$types = [
Model\Venta\Reservation\Detail\Type::Advance->value,
Model\Venta\Reservation\Detail\Type::Credit->value,
Model\Venta\Reservation\Detail\Type::Subsidy->value
];
$typesString = implode(', ', $types);
$query = $this->connection->getQueryBuilder()
->select()
->from('reservation_details')
->where("reservation_id = :id AND type IN ({$typesString})");
try {
$statement = $this->connection->execute($query, ['id' => $reservation->id]);
$results = $statement->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $exception) {
throw new EmptyResult($query, $exception, ['id' => $reservation->id, 'data' => $new_data]);
}
if (count($results) === 0) {
throw new EmptyResult($query);
}
$payment = new Model\Venta\Reservation\Payment();
foreach ($results as $result) {
switch ($result['type']) {
case Model\Venta\Reservation\Detail\Type::Advance->value:
$payment->pie = $this->advanceRepository->fetchById($result['reference_id']);
break;
case Model\Venta\Reservation\Detail\Type::Credit->value:
$payment->credito = $this->creditRepository->fetchById($result['reference_id']);
break;
case Model\Venta\Reservation\Detail\Type::Subsidy->value:
$payment->subsidio = $this->subsidyRepository->fetchById($result['reference_id']);
break;
}
}
$reservation->payment = $payment;
return $reservation;
}
}

View File

@ -1,18 +1,21 @@
<?php
namespace Incoviba\Service\Contabilidad;
use DateMalformedStringException;
use PDOException;
use DateTimeImmutable;
use DateTimeInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Log\LoggerInterface;
use PhpOffice\PhpSpreadsheet;
use Incoviba\Common\Define\Cartola\Banco;
use Incoviba\Common\Define\Contabilidad\Exporter;
use Incoviba\Common\Ideal\Service;
use Incoviba\Common\Implement\Exception;
use Incoviba\Exception\ServiceAction\Read;
use Incoviba\Model;
use Incoviba\Repository;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UploadedFileInterface;
use Psr\Log\LoggerInterface;
class Cartola extends Service
{
@ -32,6 +35,13 @@ class Cartola extends Service
$this->bancos[$name] = $banco;
return $this;
}
/**
* @param Model\Contabilidad\Banco $banco
* @param UploadedFileInterface $file
* @return array
* @throws Read
*/
public function process(Model\Contabilidad\Banco $banco, UploadedFileInterface $file): array
{
return $this->bancos[strtolower($banco->nombre)]->process($file);
@ -93,9 +103,19 @@ class Cartola extends Service
return compact('cartola', 'movimientos');
}
/**
* @param int $cuenta_id
* @param UploadedFileInterface $file
* @return array
* @throws Read
*/
public function import(int $cuenta_id, UploadedFileInterface $file): array
{
$cuenta = $this->cuentaRepository->fetchById($cuenta_id);
try {
$cuenta = $this->cuentaRepository->fetchById($cuenta_id);
} catch (Exception\EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}
$movimientos = $this->process($cuenta->banco, $file);
$inmobiliaria = $cuenta->inmobiliaria;
@ -106,15 +126,23 @@ class Cartola extends Service
if (array_key_exists('centro_costo', $dataMovimiento) and $dataMovimiento['centro_costo'] !== 0) {
$dataMovimiento['centro_costo_id'] = $dataMovimiento['centro_costo'];
}
$dataMovimiento['fecha'] = new DateTimeImmutable($dataMovimiento['fecha']);
try {
$dataMovimiento['fecha'] = new DateTimeImmutable($dataMovimiento['fecha']);
} catch (DateMalformedStringException) {
continue;
}
if (array_key_exists('rut', $dataMovimiento)) {
$ruts = $this->parseRut($dataMovimiento['rut']);
if (key_exists('rut', $ruts)) {
$dataMovimiento['rut'] = $ruts['rut'];
$dataMovimiento['digito'] = $ruts['digito'];
if ($dataMovimiento['rut'] === '') {
unset($dataMovimiento['rut']);
} else {
$dataMovimiento['rut'] = $ruts[0]['rut'];
$dataMovimiento['digito'] = $ruts[0]['digito'];
$ruts = $this->parseRut($dataMovimiento['rut']);
if (key_exists('rut', $ruts)) {
$dataMovimiento['rut'] = $ruts['rut'];
$dataMovimiento['digito'] = $ruts['digito'];
} else {
$dataMovimiento['rut'] = $ruts[0]['rut'];
$dataMovimiento['digito'] = $ruts[0]['digito'];
}
}
}
try {
@ -133,14 +161,28 @@ class Cartola extends Service
$fechas = array_unique(array_map(function($movimiento) {
return $movimiento['fecha']->format('Y-m-d');
}, $movimientos));
if (count($fechas) === 0) {
throw new Read(__CLASS__);
}
foreach ($fechas as $dia) {
try {
$this->cartolaRepository->fetchByCuentaAndFecha($cuenta->id, new DateTimeImmutable($dia));
$dayDate = new DateTimeImmutable($dia);
} catch (DateMalformedStringException) {
continue;
}
try {
$this->cartolaRepository->fetchByCuentaAndFecha($cuenta->id, $dayDate);
continue;
} catch (Exception\EmptyResult) {}
$movs = array_filter($movimientos, function($movimiento) use ($dia) {
return $movimiento['fecha'] === $dia;
return $movimiento['fecha']->format('Y-m-d') === $dia;
});
if (count($movs) === 0) {
continue;
}
$cargos = array_sum(array_map(function($movimiento) {
return $movimiento['cargo'];
}, $movs));
@ -153,11 +195,19 @@ class Cartola extends Service
'abonos' => $abonos,
'saldo' => $saldo
];
$this->buildCartola($cuenta, new DateTimeImmutable($dia), $cartolaData);
$this->buildCartola($cuenta, $dayDate, $cartolaData);
}
$startDate = new DateTimeImmutable(min($fechas));
$endDate = new DateTimeImmutable(max($fechas));
try {
$startDate = new DateTimeImmutable(min($fechas));
} catch (DateMalformedStringException $exception) {
throw new Read(__CLASS__, $exception);
}
try {
$endDate = new DateTimeImmutable(max($fechas));
} catch (DateMalformedStringException $exception) {
throw new Read(__CLASS__, $exception);
}
$movimientosFaltantes = $this->movimientoService->findMissing($cuenta, $addedMovimientos, $startDate, $endDate);
$movimientosObsoletos = [];
if (count($movimientosFaltantes) > 0) {

View File

@ -3,6 +3,7 @@ namespace Incoviba\Service\Contabilidad\Cartola;
use DateTimeImmutable;
use Incoviba\Common\Ideal\Cartola\Banco;
use Incoviba\Exception\ServiceAction\Read;
use PhpOffice\PhpSpreadsheet;
use Psr\Http\Message\UploadedFileInterface;
@ -10,8 +11,8 @@ class Itau extends Banco
{
use isExcel;
const CUENTA_CORRIENTE = 0;
const ULTIMOS_MOVIMIENTOS = 1;
const int CUENTA_CORRIENTE = 0;
const int ULTIMOS_MOVIMIENTOS = 1;
public function processMovimientosDiarios(array $movimientos): array
{
@ -44,6 +45,12 @@ class Itau extends Banco
$ext = pathinfo($uploadedFile->getClientFilename(), PATHINFO_EXTENSION);
return "/tmp/cartola.{$ext}";
}
/**
* @param string $filename
* @return array
* @throws Read
*/
protected function parseFile(string $filename): array
{
$ext = pathinfo($filename, PATHINFO_EXTENSION);
@ -62,7 +69,7 @@ class Itau extends Banco
break;
}
} catch (PhpSpreadsheet\Exception $exception) {
$this->logger->critical($exception);
throw new Read(__CLASS__, $exception);
}
return $data;
}
@ -166,6 +173,11 @@ class Itau extends Banco
});
}
/**
* @param PhpSpreadsheet\Worksheet\Worksheet $sheet
* @return int
* @throws PhpSpreadsheet\Exception
*/
protected function identifySheet(PhpSpreadsheet\Worksheet\Worksheet $sheet): int
{
foreach ($sheet->getRowIterator(1, 10) as $row) {
@ -177,7 +189,7 @@ class Itau extends Banco
return self::ULTIMOS_MOVIMIENTOS;
}
}
throw new PhpSpreadsheet\Exception();
throw new PhpSpreadsheet\Exception('Incorrect type of Worksheet');
}
protected function getDateRange(PhpSpreadsheet\Worksheet\Row $row): array
{

View File

@ -5,11 +5,13 @@ use DateTimeInterface;
use DateTimeImmutable;
use DateInterval;
use Incoviba\Common\Implement\Exception\EmptyRedis;
use Psr\Log\LoggerInterface;
class IPC
{
protected string $redisKey = 'ipc';
public function __construct(protected Redis $redisService, protected Money $moneyService) {}
public function __construct(protected LoggerInterface $logger, protected Redis $redisService,
protected Money $moneyService) {}
public function get(DateTimeInterface $from, DateTimeInterface $to = new DateTimeImmutable()): float
{
@ -21,7 +23,7 @@ class IPC
$ipcs = [];
try {
$ipcs = json_decode($this->redisService->get($this->redisKey), JSON_OBJECT_AS_ARRAY);
if (!isset($ipcs[$dateKey])) {
if (!isset($ipcs[$dateKey]) or $ipcs[$dateKey] === 0) {
throw new EmptyRedis($this->redisKey);
}
} catch (EmptyRedis) {
@ -33,7 +35,7 @@ class IPC
ksort($ipcs);
$this->redisService->set($this->redisKey, json_encode($ipcs), 60 * 60 * 24 * 30);
}
return $ipcs[$dateKey];
return $ipcs[$dateKey] * ($ipcs[$dateKey] > 1 ? 1/100 : 1);
}
public function readjust(float $base, DateTimeInterface $from, DateTimeInterface $to = new DateTimeImmutable()):float
{

View File

@ -8,32 +8,29 @@ use Incoviba\Common\Implement\Exception\EmptyResponse;
class Money
{
const UF = 'uf';
const USD = 'usd';
const IPC = 'ipc';
const string UF = 'uf';
const string USD = 'usd';
const string IPC = 'ipc';
public function __construct(protected LoggerInterface $logger) {}
protected array $providers;
public function register(string $name, Provider $provider): Money
protected array $providers = [];
public function register(Provider $provider): Money
{
if (isset($this->providers) and isset($this->providers[$name]) and in_array($provider, $this->providers[$name])) {
if (in_array($provider, $this->providers)) {
return $this;
}
if (!isset($this->providers[$name])) {
$this->providers[$name] = [];
}
$this->providers[$name] []= $provider;
$this->providers []= $provider;
return $this;
}
public function getProviders(string $name): array
{
return $this->providers[$name];
return array_values(array_filter($this->providers, fn($provider) => $provider->supported($name)));
}
public function get(string $provider, DateTimeInterface $dateTime): float
public function get(string $name, DateTimeInterface $dateTime): float
{
$providers = $this->getProviders($provider);
$providers = $this->getProviders($name);
foreach ($providers as $provider) {
try {
return $provider->get(self::getSymbol($provider), $dateTime);
@ -45,7 +42,7 @@ class Money
}
public function getUF(?DateTimeInterface $dateTime = null): float
{
$providers = $this->getProviders('uf');
$providers = $this->getProviders(self::UF);
foreach ($providers as $provider) {
try {
return $provider->get(self::UF, $dateTime);
@ -60,10 +57,13 @@ class Money
if ($start >= $end) {
return 0;
}
$providers = $this->getProviders('ipc');
$providers = $this->getProviders(self::IPC);
foreach ($providers as $provider) {
try {
return $provider->getVar($start, $end);
if (method_exists($provider, 'getVar')) {
return $provider->getVar($start, $end);
}
return $provider->get(self::IPC, $end);
} catch (EmptyResponse $exception) {
$this->logger->notice($exception);
}
@ -72,7 +72,7 @@ class Money
}
public function getUSD(DateTimeInterface $dateTime): float
{
$providers = $this->getProviders('usd');
$providers = $this->getProviders(self::USD);
foreach ($providers as $provider) {
try {
return $provider->get(self::USD, $dateTime);

View File

@ -0,0 +1,112 @@
<?php
namespace Incoviba\Service\Money;
use DateTimeInterface;
use DateTimeImmutable;
use JsonException;
use Psr\Http\Client\ClientExceptionInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Ideal;
use Incoviba\Common\Implement\Exception\EmptyResponse;
use Incoviba\Service\Money;
class Findic extends Ideal\Money\Provider
{
public function __construct(LoggerInterface $logger, protected ClientInterface $client)
{
parent::__construct($logger);
}
protected string $url = 'https://findic.cl/api/';
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float
{
if (!$this->supported($money_symbol)) {
throw new EmptyResponse($money_symbol);
}
if ($dateTime === null) {
$dateTime = new DateTimeImmutable();
}
return match (strtolower($money_symbol)) {
Money::UF, Money::USD => $this->getDate(strtolower($money_symbol), $dateTime),
Money::IPC => $this->getSum(strtolower($money_symbol), $dateTime,
DateTimeImmutable::createFromFormat('Y-m-d',
$dateTime->modify('-1 year')->format('Y-11-01'))),
default => throw new EmptyResponse($money_symbol),
};
}
protected array $supportedMap = [
Money::UF => 'uf',
Money::IPC => 'ipc',
Money::USD => 'dolar'
];
/**
* @param string $request_uri
* @return float
* @throws EmptyResponse
*/
protected function sendRequest(string $request_uri): array
{
try {
$response = $this->client->get($request_uri);
} catch (ClientExceptionInterface $exception) {
throw new EmptyResponse($request_uri, $exception);
}
if ((int) floor($response->getStatusCode() / 100) !== 2) {
throw new EmptyResponse($request_uri);
}
$body = $response->getBody();
try {
$json = json_decode($body->getContents(), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $exception) {
throw new EmptyResponse($request_uri, $exception);
}
if (!array_key_exists('serie', $json) or count($json['serie']) === 0) {
throw new EmptyResponse($request_uri);
}
return $json['serie'];
}
/**
* @param string $symbol
* @param DateTimeInterface $dateTime
* @return float
* @throws EmptyResponse
*/
protected function getDate(string $symbol, DateTimeInterface $dateTime): float
{
$request_uri = "{$this->supportedMap[strtolower($symbol)]}/{$dateTime->format('d-m-Y')}";
return (float) $this->sendRequest($request_uri)[0]['valor'];
}
/**
* @param string $symbol
* @param DateTimeInterface $dateTime
* @param DateTimeInterface|null $fromDateTime
* @return float
* @throws EmptyResponse
*/
protected function getSum(string $symbol, DateTimeInterface $dateTime, ?DateTimeInterface $fromDateTime = null): float
{
$dateTime = $dateTime->modify('last day of this month');
$requestUri = "{$this->supportedMap[strtolower($symbol)]}/{$dateTime->format('Y')}";
$serie = $this->sendRequest($requestUri);
$values = array_filter($serie, fn($month) => DateTimeImmutable::createFromFormat('Y-m-d', $month['fecha']) <= $dateTime);
$value = array_reduce($values, fn($value, $month) => $value + ((float) $month['valor']), 0.0);
if ($fromDateTime === null) {
$fromDateTime = $dateTime->modify('-1 year');
} else {
$fromDateTime = $fromDateTime->modify('last day of this month');
}
$requestUri = "{$this->supportedMap[strtolower($symbol)]}/{$fromDateTime->format('Y')}";
$serie = $this->sendRequest($requestUri);
$values = array_filter($serie, fn($month) => DateTimeImmutable::createFromFormat('Y-m-d', $month['fecha']) > $fromDateTime);
return array_reduce($values, fn($value, $month) => $value + ((float) $month['valor']), $value);
}
}

View File

@ -6,14 +6,19 @@ use DateTimeInterface;
use DateTimeImmutable;
use DateInterval;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use GuzzleHttp\Exception\GuzzleException;
use Incoviba\Common\Implement\Exception\EmptyResponse;
use Incoviba\Common\Define\Money\Provider;
use Incoviba\Common\Ideal;
use Incoviba\Service\Money;
class Ine implements Provider
class Ine extends Ideal\Money\Provider
{
protected string $uri = 'https://api-calculadora.ine.cl/ServiciosCalculadoraVariacion';
public function __construct(protected ClientInterface $client) {}
public function __construct(LoggerInterface $logger, protected ClientInterface $client)
{
parent::__construct($logger);
}
/**
* @throws EmptyResponse
@ -21,6 +26,9 @@ class Ine implements Provider
*/
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float
{
if (!$this->supported($money_symbol)) {
throw new EmptyResponse($money_symbol);
}
if ($dateTime === null) {
$dateTime = new DateTimeImmutable();
}
@ -29,6 +37,10 @@ class Ine implements Provider
return $this->getVar($start, $end);
}
protected array $supportedMap = [
Money::IPC => 'ipc'
];
/**
* @throws EmptyResponse
*/

View File

@ -3,22 +3,33 @@ namespace Incoviba\Service\Money;
use DateTimeInterface;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use GuzzleHttp\Exception\GuzzleException;
use Incoviba\Common\Define\Money\Provider;
use Incoviba\Common\Ideal;
use Incoviba\Common\Implement\Exception\EmptyResponse;
use Incoviba\Service\Money;
class MiIndicador implements Provider
class MiIndicador extends Ideal\Money\Provider
{
public function __construct(protected ClientInterface $client) {}
public function __construct(LoggerInterface $logger, protected ClientInterface $client)
{
parent::__construct($logger);
}
/**
* @param string $money_symbol
* @param DateTimeInterface|null $dateTime
* @return float
* @throws EmptyResponse
*/
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float
{
$request_uri = "{$money_symbol}";
if (!$this->supported($money_symbol)) {
throw new EmptyResponse($money_symbol);
}
$request_uri = "{$this->supportedMap[strtolower($money_symbol)]}";
if ($dateTime !== null) {
$request_uri = "{$money_symbol}/{$dateTime->format('d-m-Y')}";
$request_uri = "{$request_uri}/{$dateTime->format('d-m-Y')}";
}
try {
$response = $this->client->get($request_uri);
@ -33,9 +44,14 @@ class MiIndicador implements Provider
$body = $response->getBody();
$json = json_decode($body->getContents());
if (empty($json) or $json->codigo !== $money_symbol or count($json->serie) === 0) {
if (empty($json) or !isset($json->codigo) or !isset($json->serie) or $json->codigo !== $money_symbol or count($json->serie) === 0) {
throw new EmptyResponse($request_uri);
}
return $json->serie[0]->valor;
}
protected array $supportedMap = [
Money::UF => 'uf',
Money::IPC => 'ipc',
Money::USD => 'dolar'
];
}

View File

@ -6,27 +6,38 @@ use DateTimeImmutable;
use PDO;
use PDOException;
use Psr\Http\Client\ClientInterface;
use Psr\Log\LoggerInterface;
use Dom\HTMLDocument;
use GuzzleHttp\Exception\GuzzleException;
use Incoviba\Common\Implement\Exception\EmptyResponse;
use Incoviba\Common\Define;
use Incoviba\Common\Ideal;
use Incoviba\Repository;
use Incoviba\Service;
class SII implements Define\Money\Provider
class SII extends Ideal\Money\Provider
{
public function __construct(protected ClientInterface $client,
protected Repository\UF $ufRepository) {}
public function __construct(LoggerInterface $logger, protected ClientInterface $client,
protected Repository\UF $ufRepository)
{
parent::__construct($logger);
}
public function get(string $money_symbol, ?DateTimeInterface $dateTime = null): float
{
if ($money_symbol === Service\Money::UF) {
if (!$this->supported($money_symbol)) {
throw new EmptyResponse("{$money_symbol} not found in " . __CLASS__);
}
if (strtolower($money_symbol) === Service\Money::UF) {
return $this->getUF($dateTime);
}
$class = __CLASS__;
throw new EmptyResponse("{$money_symbol} not found in {$class}");
}
protected array $supportedMap = [
Service\Money::UF => 'uf'
];
/**
* @param DateTimeInterface|null $dateTime
* @return float

View File

@ -236,6 +236,9 @@ class Persona extends Ideal\Service
} else {
$newKey = substr($key, strlen('address_'));
}
if ($newKey === 'id') {
continue;
}
$addressData[$newKey] = $value;
}
if (!empty($addressData)) {
@ -258,7 +261,7 @@ class Persona extends Ideal\Service
'birthdate' => 'fecha_nacimiento',
];
foreach ($data as $key => $value) {
if (array_key_exists($key, $dataMap)) {
if (array_key_exists($key, $dataMap) and trim($value) !== '') {
$data[$dataMap[$key]] = $value;
unset($data[$key]);
}

View File

@ -79,13 +79,7 @@ class Queue extends Ideal\Service
try {
$this->jobService->update($job);
} catch (Update $exception) {
$this->logger->error($exception->getMessage(), ['job' => $job, 'exception' => [
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'trace' => $exception->getTraceAsString(),
]]);
$this->logger->error($exception->getMessage(), ['job' => $job, 'exception' => $exception]);
}
return false;
}

View File

@ -22,7 +22,7 @@ class UF
if ($date === null) {
$date = new DateTimeImmutable();
}
if ($date->diff($today)->days < 0) {
if ($date->diff($today)->invert === 1) {
return 0.0;
}
/**
@ -32,7 +32,7 @@ class UF
*/
try {
$ufs = $this->getRedisUFs();
if (!isset($ufs[$date->format('Y-m-d')])) {
if (!isset($ufs[$date->format('Y-m-d')]) or $ufs[$date->format('Y-m-d')] === 0) {
throw new EmptyRedis($this->redisKey);
}
return $ufs[$date->format('Y-m-d')];
@ -54,12 +54,16 @@ class UF
}
public function updateMany(array $dates): array
{
$today = new DateTimeImmutable();
$ufs = [];
try {
$ufs = json_decode($this->redisService->get($this->redisKey), JSON_OBJECT_AS_ARRAY);
} catch (EmptyRedis) {}
$updated = [];
foreach ($dates as $date) {
if ($date->diff($today)->invert === 1) {
continue;
}
if (!isset($ufs[$date->format('Y-m-d')]) or $ufs[$date->format('Y-m-d')] === 0) {
$uf = $this->moneyService->getUF($date);
if ($uf === 0.0) {

View File

@ -19,7 +19,7 @@ class USD
$usds = [];
try {
$usds = json_decode($this->redisService->get($this->redisKey), JSON_OBJECT_AS_ARRAY);
if (!isset($usds[$date->format('Y-m-d')])) {
if (!isset($usds[$date->format('Y-m-d')]) or $usds[$date->format('Y-m-d')] === 0) {
throw new EmptyRedis($this->redisKey);
}
$usd = $usds[$date->format('Y-m-d')];

View File

@ -4,6 +4,7 @@ namespace Incoviba\Service\Venta;
use DateMalformedStringException;
use Exception;
use DateTimeImmutable;
use Incoviba\Exception\ServiceAction\Update;
use PDOException;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Ideal;
@ -55,7 +56,11 @@ class Credito extends Ideal\Service
} catch (DateMalformedStringException) {
$fecha = new DateTimeImmutable();
}
$uf = $this->valorService->clean($data['uf']) ?? $this->moneyService->getUF($fecha);
if (array_key_exists('uf', $data)) {
$uf = $this->valorService->clean($data['uf']) ?? $this->moneyService->getUF($fecha);
} else {
$uf = $this->moneyService->getUF($fecha);
}
try {
$tipoPago = $this->tipoPagoRepository->fetchByDescripcion('carta de resguardo');
} catch (EmptyResult $exception) {
@ -85,13 +90,20 @@ class Credito extends Ideal\Service
}
/**
* @throws Exception
* @param Model\Venta\Credito $credito
* @param array $data
* @return Model\Venta\Credito
* @throws Update
*/
public function edit(Model\Venta\Credito $credito, array $data): Model\Venta\Credito
{
$uf = $this->moneyService->getUF($credito->pago->fecha);
if (array_key_exists('fecha', $data)) {
$fecha = new DateTimeImmutable($data['fecha']);
try {
$fecha = new DateTimeImmutable($data['fecha']);
} catch (DateMalformedStringException $exception) {
throw new Update(__CLASS__, $exception);
}
$data['fecha'] = $fecha->format('Y-m-d');
$uf = $this->moneyService->getUF($fecha);
$data['uf'] = $uf;
@ -112,6 +124,10 @@ class Credito extends Ideal\Service
}
$credito->pago = $this->pagoService->edit($credito->pago, $filteredDataPago);
return $this->creditoRepository->edit($credito, $filteredData);
try {
return $this->creditoRepository->edit($credito, $filteredData);
} catch (EmptyResult $exception) {
throw new Update(__CLASS__, $exception);
}
}
}

View File

@ -4,6 +4,8 @@ namespace Incoviba\Service\Venta;
use Exception;
use DateTimeImmutable;
use DateMalformedStringException;
use Incoviba\Exception\ServiceAction\Read;
use Incoviba\Exception\ServiceAction\Update;
use Psr\Log\LoggerInterface;
use Incoviba\Common\Ideal;
use Incoviba\Common\Implement\Exception\EmptyResult;
@ -56,13 +58,15 @@ class Escritura extends Ideal\Service
/**
* @throws EmptyResult
* @throws Read
* @throws Update
*/
public function edit(int $venta_id, array $data): Model\Venta\Escritura
{
$venta = $this->ventaService->getById($venta_id);
$estado = $venta->currentEstado();
if (!in_array($estado->tipoEstadoVenta->descripcion, ['escriturando', 'firmado por inmobiliaria'])) {
throw new EmptyResult('');
throw new Update(__CLASS__);
}
try {
$data['fecha'] = (new DateTimeImmutable($data['fecha']))->format('Y-m-d');

View File

@ -116,8 +116,8 @@ class Pago extends Ideal\Service\Repository
try {
$pago = $this->pagoRepository->fetchById($pago_id);
return $this->process($pago);
} catch (EmptyResult) {
throw new Read(__CLASS__);
} catch (EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}
}

View File

@ -72,22 +72,22 @@ class Import extends Ideal\Service
}
$baseTitlesMap = [
'proyecto' => [],
'precio' => [],
'precio' => ['valor'],
'unidad' => ['departamento'],
'tipo' => ['tipo unidad'],
'fecha' => []
'fecha' => [],
];
$realTitlesMap = [];
$titles = array_keys($data[0]);
foreach ($baseTitlesMap as $base => $optionals) {
foreach ($titles as $title) {
if (str_contains($title, $base)) {
if (str_contains(strtolower($title), $base)) {
$realTitlesMap[$title] = $base;
break;
}
foreach ($optionals as $optional) {
if (str_contains($title, $optional)) {
if (str_contains(strtolower($title), $optional)) {
$realTitlesMap[$title] = $base;
break;
}

View File

@ -22,7 +22,9 @@ class Reservation extends Ideal\Service\API
protected Repository\Venta\Reservation\State $stateRepository,
protected Service\Persona $personaService,
protected Service\Proyecto\Broker $brokerService,
protected Promotion $promotionService, protected Unidad $unitService)
protected Promotion $promotionService, protected Unidad $unitService,
protected Pie $pieService, protected Credito $creditService,
protected Subsidio $subsidyService)
{
parent::__construct($logger);
}
@ -116,81 +118,36 @@ class Reservation extends Ideal\Service\API
}
}
/**
* @param array $data
* @return Model\Venta\Reservation
* @throws ServiceAction\Create
*/
public function add(array $data): Model\Venta\Reservation
{
$date = new DateTimeImmutable();
$date = $this->parseDate($data['date'] ?? 'now');
try {
$date = new DateTimeImmutable($data['date']);
} catch (DateMalformedStringException) {}
try {
$reservation = $this->reservationRepository->fetchByBuyerAndUnitAndDate($data['buyer_rut'], (int) $data['units'][0], $date);
if (array_key_exists('broker_rut', $data) and $data['broker_rut'] !== '') {
try {
$broker = $this->brokerService->get($data['broker_rut']);
$reservation = $this->reservationRepository->edit($reservation, ['broker_rut' => $broker->rut]);
} catch (ServiceAction\Read) {}
}
} catch (Implement\Exception\EmptyResult) {
if (!$this->reservationRepository->getConnection()->getPDO()->inTransaction()) {
$this->reservationRepository->getConnection()->getPDO()->beginTransaction();
}
$buyerData = [];
foreach ($data as $key => $value) {
if (!str_starts_with($key, 'buyer_')) {
continue;
}
$buyerData[substr($key, strlen('buyer_'))] = $value;
}
$this->personaService->add($buyerData);
$data['date'] = $date->format('Y-m-d');
try {
$reservationData = $this->reservationRepository->filterData($data);
$reservation = $this->reservationRepository->create($reservationData);
$reservation = $this->reservationRepository->save($reservation);
$stateType = Model\Venta\Reservation\State\Type::INACTIVE;
$stateData = [
'reservation_id' => $reservation->id,
'date' => $data['date'],
'type' => $stateType->value,
];
$state = $this->stateRepository->create($stateData);
$this->stateRepository->save($state);
$units = array_combine($data['units'], $data['units_value']);
$this->addUnits($reservation, $units);
if (array_key_exists('broker_rut', $data) and !empty($data['broker_rut'])) {
$this->addBroker($reservation, $data['broker_rut']);
}
if (array_key_exists('promotions', $data)) {
$this->addPromotions($reservation, $data['promotions']);
}
if ($this->reservationRepository->getConnection()->getPDO()->inTransaction()) {
$this->reservationRepository->getConnection()->getPDO()->commit();
}
} catch (PDOException $exception) {
$this->logger->warning($exception->getMessage(), ['exception' => $exception->getTraceAsString()]);
if ($this->reservationRepository->getConnection()->getPDO()->inTransaction()) {
$this->reservationRepository->getConnection()->getPDO()->rollBack();
}
throw new ServiceAction\Create(__CLASS__, $exception);
}
return $this->updateExistingReservation($data, $date);
} catch (Read | ServiceAction\Update) {
return $this->createNewReservation($data, $date);
}
return $this->process($reservation);
}
public function edit(Define\Model $model, array $new_data): Model\Venta\Reservation
public function edit(Define\Model $model, array $newData): Model\Venta\Reservation
{
$editData = [];
foreach ($newData as $key => $value) {
$newKey = str_replace('edit_', '', $key);
$editData[$newKey] = $value;
}
try {
return $this->process($this->reservationRepository->edit($model, $new_data));
$reservation = $this->reservationRepository->edit($model, $editData);
} catch (PDOException | Implement\Exception\EmptyResult $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
$paymentData = array_filter($editData, fn($key) => str_starts_with($key, 'payment_'), ARRAY_FILTER_USE_KEY);
$this->editPayment($reservation, $paymentData);
return $this->process($reservation);
}
public function delete(int $id): Model\Venta\Reservation
{
@ -251,6 +208,12 @@ class Reservation extends Ideal\Service\API
return $this->stateRepository->fetchByReservation($reservation_id);
})
);
foreach ($model->units as &$unit) {
$unit->unit = $this->unitService->getById($unit->unit->id);
}
foreach ($model->promotions as &$promotion) {
$promotion = $this->promotionService->getById($promotion->id);
}
$model->buyer = $this->personaService->getById($model->buyer->rut);
return $model;
}
@ -274,6 +237,13 @@ class Reservation extends Ideal\Service\API
$this->reservationRepository->save($reservation);
} catch (ServiceAction\Read) {}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $promotions
* @return void
* @throws PDOException
*/
protected function addPromotions(Model\Venta\Reservation $reservation, array $promotions): void
{
foreach ($promotions as $promotion_id) {
@ -286,4 +256,434 @@ class Reservation extends Ideal\Service\API
}
$this->reservationRepository->save($reservation);
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $payment
* @return void
* @throws ServiceAction\Create
*/
protected function addPayment(Model\Venta\Reservation $reservation, array $payment): void
{
$fields = [
'pie',
'advance',
'cuotas',
'investments',
'credito',
'credit',
'subsidio',
'subsidy',
'ahorro',
'savings',
];
$filteredData = array_intersect_key($payment, array_flip($fields));
$map = [
'pie' => 'advance',
'cuotas' => 'investments',
'credito' => 'credit',
'subsidio' => 'subsidy',
'ahorro' => 'savings'
];
$mappedData = [];
foreach ($filteredData as $key => $value) {
$mappedData[$map[$key]] = $value;
}
if (array_key_exists('advance', $mappedData)) {
$this->addAdvance($reservation, $mappedData);
}
if (array_key_exists('credit', $mappedData)) {
$this->addCredit($reservation, $mappedData);
}
if (array_key_exists('subsidy', $mappedData)) {
$this->addSubsidy($reservation, $mappedData);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Update
*/
protected function editPayment(Model\Venta\Reservation $reservation, array $data): void
{
$fields = [
'pie',
'advance',
'cuotas',
'investments',
'credito',
'credit',
'subsidio',
'subsidy',
'ahorro',
'savings',
];
$filteredData = [];
foreach ($data as $key => $value) {
$newKey = str_replace('payment_', '', $key);
if (in_array($newKey, $fields)) {
$filteredData[$newKey] = $value;
}
}
$map = [
'pie' => 'advance',
'cuotas' => 'investments',
'credito' => 'credit',
'subsidio' => 'subsidy',
'ahorro' => 'savings'
];
$mappedData = [];
foreach ($filteredData as $key => $value) {
if (array_key_exists($key, $map)) {
$mappedData[$map[$key]] = $value;
continue;
}
$mappedData[$key] = $value;
}
if (array_key_exists('advance', $mappedData)) {
$this->editAdvance($reservation, $mappedData);
} else {
$this->removeAdvance($reservation);
}
if (array_key_exists('credit', $mappedData)) {
$this->editCredit($reservation, $mappedData);
} else {
$this->removeCredit($reservation);
}
/*if (array_key_exists('subsidy', $mappedData)) {
$this->editSubsidy($reservation, $mappedData);
}*/
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Create
*/
protected function addAdvance(Model\Venta\Reservation $reservation, array $data): void
{
$fields = [
'advance',
'investments'
];
$filteredData = array_intersect_key($data, array_flip($fields));
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'valor' => $filteredData['advance'],
'cuotas' => $filteredData['investments']
];
$pie = $this->pieService->add($inputData);
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Advance->value, $pie->id);
} catch (PDOException $exception) {
throw new ServiceAction\Create(__CLASS__, $exception);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Update
*/
protected function editAdvance(Model\Venta\Reservation $reservation, array $data): void
{
$fields = [
'advance',
'investments'
];
$filteredData = array_intersect_key($data, array_flip($fields));
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'valor' => $filteredData['advance'],
'cuotas' => $filteredData['investments']
];
if (isset($reservation->payment) and isset($reservation->payment->pie)) {
$this->pieService->edit($reservation->payment->pie, $inputData);
return;
}
try {
$pie = $this->pieService->add($inputData);
} catch (ServiceAction\Create $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Advance->value, $pie->id);
} catch (PDOException $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
}
protected function removeAdvance(Model\Venta\Reservation $reservation): void
{
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Create
*/
protected function addCredit(Model\Venta\Reservation $reservation, array $data): void
{
$creditValue = $data['credit'];
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'valor' => $creditValue
];
$credit = $this->creditService->add($inputData);
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Credit->value, $credit->id);
} catch (PDOException $exception) {
throw new ServiceAction\Create(__CLASS__, $exception);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Update
*/
protected function editCredit(Model\Venta\Reservation $reservation, array $data): void
{
$creditValue = $data['credit'];
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'valor' => $creditValue
];
if (isset($reservation->payment) and isset($reservation->payment->credito)) {
$this->creditService->edit($reservation->payment->credito, $inputData);
return;
}
try {
$credit = $this->creditService->add($inputData);
} catch (ServiceAction\Create $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Credit->value, $credit->id);
} catch (PDOException $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
}
protected function removeCredit(Model\Venta\Reservation $reservation): void
{
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Create
*/
protected function addSubsidy(Model\Venta\Reservation $reservation, array $data): void
{
$fields = [
'subsidy',
'savings'
];
$filteredData = array_intersect_key($data, array_flip($fields));
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'subsidio' => $filteredData['subsidy'],
'ahorro' => $filteredData['savings']
];
$subsidy = $this->subsidyService->add($inputData);
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Subsidy->value, $subsidy->id);
} catch (PDOException $exception) {
throw new ServiceAction\Create(__CLASS__, $exception);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @param array $data
* @return void
* @throws ServiceAction\Update
*/
/*protected function editSubsidy(Model\Venta\Reservation $reservation, array $data): void
{
$fields = [
'subsidy',
'savings'
];
$filteredData = array_intersect_key($data, array_flip($fields));
$inputData = [
'fecha' => $reservation->date->format('Y-m-d'),
'subsidio' => $filteredData['subsidy'],
'ahorro' => $filteredData['savings']
];
if (isset($reservation->payment) and isset($reservation->payment->subsidy)) {
$this->subsidyService->edit($reservation->payment->subsidy, $inputData);
return;
}
try {
$subsidy = $this->subsidyService->add($inputData);
} catch (ServiceAction\Create $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
try {
$this->reservationRepository->saveDetail($reservation, Model\Venta\Reservation\Detail\Type::Subsidy->value, $subsidy->id);
} catch (PDOException $exception) {
throw new ServiceAction\Update(__CLASS__, $exception);
}
}*/
/**
* @param array $data
* @param DateTimeImmutable $date
* @return Model\Venta\Reservation
* @throws Read
* @throws ServiceAction\Update
*/
protected function updateExistingReservation(array $data, DateTimeImmutable $date): Model\Venta\Reservation
{
try {
$reservation = $this->reservationRepository->fetchByBuyerAndUnitAndDate(
$data['buyer_rut'],
(int) ($data['units'][0] ?? 0),
$date
);
} catch (Implement\Exception\EmptyResult $exception) {
throw new Read(__CLASS__, $exception);
}
if (!empty($data['broker_rut'] ?? null)) {
$this->updateBrokerForReservation($reservation, $data['broker_rut']);
}
return $this->process($reservation);
}
/**
* @param array $data
* @param DateTimeImmutable $date
* @return Model\Venta\Reservation
* @throws ServiceAction\Create
*/
protected function createNewReservation(array $data, DateTimeImmutable $date): Model\Venta\Reservation
{
$pdo = $this->reservationRepository->getConnection()->getPDO();
$inTransaction = $pdo->inTransaction();
try {
if (!$inTransaction) {
$pdo->beginTransaction();
}
$this->createOrUpdateBuyer($data);
$reservation = $this->persistReservation($data, $date->format('Y-m-d'));
$this->createInitialState($reservation, $date);
$this->processReservationDetails($reservation, $data);
if (!$inTransaction) {
$pdo->commit();
}
return $this->process($reservation);
} catch (PDOException $exception) {
if (!$inTransaction && $pdo->inTransaction()) {
$pdo->rollBack();
}
$this->logger->error('Failed to create reservation', [
'error' => $exception->getMessage(),
'data' => $this->sanitizeData($data)
]);
throw new ServiceAction\Create(__CLASS__, $exception);
}
}
protected function parseDate(?string $dateString): DateTimeImmutable
{
try {
return new DateTimeImmutable($dateString);
} catch (\Exception) {
return new DateTimeImmutable();
}
}
/**
* @param array $data
* @return void
* @throws ServiceAction\Create
*/
protected function createOrUpdateBuyer(array $data): void
{
$buyerData = array_filter($data, fn($key) => str_starts_with($key, 'buyer_'), ARRAY_FILTER_USE_KEY);
$buyerData = array_combine(
array_map(fn($key) => substr($key, strlen('buyer_')), array_keys($buyerData)),
$buyerData
);
$this->personaService->add($buyerData);
}
protected function persistReservation(array $data, string $date): Model\Venta\Reservation
{
$data['date'] = $date;
$reservationData = $this->reservationRepository->filterData($data);
$reservation = $this->reservationRepository->create($reservationData);
return $this->reservationRepository->save($reservation);
}
protected function createInitialState(Model\Venta\Reservation $reservation, DateTimeInterface $date): void
{
$stateData = [
'reservation_id' => $reservation->id,
'date' => $date->format('Y-m-d'),
'type' => Model\Venta\Reservation\State\Type::INACTIVE->value,
];
$state = $this->stateRepository->create($stateData);
$this->stateRepository->save($state);
}
protected function processReservationDetails(Model\Venta\Reservation $reservation, array $data): void
{
if (!empty($data['units']) && !empty($data['units_value'])) {
$units = array_combine($data['units'], $data['units_value']);
$this->addUnits($reservation, $units);
}
if (!empty($data['broker_rut'])) {
$this->addBroker($reservation, $data['broker_rut']);
}
if (!empty($data['promotions'])) {
$this->addPromotions($reservation, $data['promotions']);
}
$paymentData = array_filter($data, fn($key) => str_starts_with($key, 'payment_'), ARRAY_FILTER_USE_KEY);
if (!empty($paymentData)) {
$this->addPayment($reservation, $paymentData);
}
}
/**
* @param Model\Venta\Reservation $reservation
* @param string $brokerRut
* @return void
* @throws ServiceAction\Update
*/
protected function updateBrokerForReservation(Model\Venta\Reservation $reservation, string $brokerRut): void
{
try {
$broker = $this->brokerService->get($brokerRut);
$this->reservationRepository->edit($reservation, ['broker_rut' => $broker->rut]);
} catch (Implement\Exception\EmptyResult | ServiceAction\Read $exception) {
$this->logger->warning('Broker not found', ['broker_rut' => $brokerRut]);
throw new ServiceAction\Update(__CLASS__, $exception);
}
}
protected function sanitizeData(array $data): array
{
$sensitiveFields = ['password', 'token', 'api_key', 'rut'];
return array_map(
fn($value, $key) => in_array($key, $sensitiveFields, true) ? '***' : $value,
$data,
array_keys($data)
);
}
}