Subir saldos como csv

This commit is contained in:
Rodrigo 2025-06-15 00:19:30 -03:00
parent 6787dde711
commit b752f9e8c5
4 changed files with 154 additions and 1 deletions

View file

@ -8,10 +8,14 @@ use App\Producto;
use Illuminate\Http\JsonResponse; use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\BinaryFileResponse;
use League\Csv\Reader;
use DatabaseSeeder;
class ComprasController class ComprasController
{ {
const CANASTAS_PATH = 'csv/canastas/'; const CANASTAS_PATH = 'csv/canastas/';
const BARRIO = "Barrio";
const SALDO = "Saldo";
public function indexPedidos() { public function indexPedidos() {
return view('compras_pedidos'); return view('compras_pedidos');
@ -59,4 +63,36 @@ class ComprasController
$file = resource_path('csv/productos.csv'); $file = resource_path('csv/productos.csv');
return response()->download($file); return response()->download($file);
} }
public function cargarSaldos(Request $request): JsonResponse
{
$request->validate([
'data' => 'required|file|mimes:csv,txt|max:2048',
]);
$file = $request->file('data')->getPathname();
$csv = Reader::createFromPath($file, 'r');
try {
$csv->setDelimiter("|");
$csv->setEnclosure("'");
$csv->setHeaderOffset(0);
$records = $csv->getRecords();
} catch (InvalidArgument|Exception $e) {
Log::error($e->getMessage());
return response()->json([
'message' => 'No se pudo leer el csv',
]);
}
foreach ($records as $record) {
$barrio = $record[self::BARRIO];
$saldo = $record[self::SALDO];
GrupoDeCompra::where('nombre', $barrio)
->update(['saldo' => $saldo]);
}
return response()->json([
'message' => 'Saldos cargados exitosamente',
]);
}
} }

View file

@ -47,7 +47,38 @@
:class="seccionActiva === 'saldos-compras-seccion' ? 'is-active' : 'is-hidden'" :class="seccionActiva === 'saldos-compras-seccion' ? 'is-active' : 'is-hidden'"
> >
<div class="block" id="saldos-compras-seccion"> <div class="block" id="saldos-compras-seccion">
<table class="table"> <article class="message is-warning">
<div class="message-header">
<p>
<button class="icon" aria-label="foldout" @click="toggleSaldosFileDialog">
<i class="fa" :class="show_saldos_file_dialog ? 'fa-arrow-up' : 'fa-arrow-down'"></i>
</button>
Cargar saldos
</p>
</div>
<div class="message-body" v-if="show_saldos_file_dialog">
<div class="content">
La planilla de saldos tiene que tener el siguiente formato para que la aplicación la lea correctamente:
<ul>
<li>Los valores deben usar punto y no coma decimal</li>
<li>El nombre de las columnas deben ser "Barrio" y "Saldo"</li>
<li>Las celdas deben separarse con '|'</li>
<li>No puede haber "enters" en ninguna celda</li>
<li>El nombre de los barrios debe estar exactamente igual que como est&aacute;n configurados en esta aplicacai&oacute;n</li>
</ul>
<article class="message is-danger mt-2">
<div class="message-body">
<div class="content">
Cargar un archivo de saldos s&oacute;lo reemplazar&aacute; los saldos de los barrios presentes en la tabla.
</div>
</div>
</article>
</div>
<input-file-button text="Subir archivo" @archivo-subido="saldosSubido" />
</div>
</article>
<table class="table container">
<thead> <thead>
<tr> <tr>
<th>Barrio</th> <th>Barrio</th>
@ -84,6 +115,7 @@
import TabsSecciones from "../comunes/TabsSecciones.vue"; import TabsSecciones from "../comunes/TabsSecciones.vue";
import DropdownDescargar from "./DropdownDescargar.vue"; import DropdownDescargar from "./DropdownDescargar.vue";
import CanastaInput from "./CanastaInput.vue"; import CanastaInput from "./CanastaInput.vue";
import InputFileButton from "../comunes/InputFileButton.vue";
import { mapActions, mapState } from "vuex"; import { mapActions, mapState } from "vuex";
export default { export default {
@ -104,6 +136,7 @@ export default {
seccionActiva: "pedidos-compras-seccion", seccionActiva: "pedidos-compras-seccion",
archivo: undefined, archivo: undefined,
saldo_modificado: {}, saldo_modificado: {},
show_saldos_file_dialog: false,
} }
}, },
methods: { methods: {
@ -112,6 +145,7 @@ export default {
this.seccionActiva = tabId + "-seccion"; this.seccionActiva = tabId + "-seccion";
}, },
...mapActions('comisiones', ['getGruposDeCompra', 'setSaldo']), ...mapActions('comisiones', ['getGruposDeCompra', 'setSaldo']),
...mapActions('ui',["toast"]),
async confirmar_saldo(gdc_id) { async confirmar_saldo(gdc_id) {
var saldo = this.getSaldo(gdc_id); var saldo = this.getSaldo(gdc_id);
await this.setSaldo({ await this.setSaldo({
@ -135,6 +169,32 @@ export default {
} }
return 0; return 0;
}, },
async saldosSubido(event) {
var archivo = event.archivo;
if (archivo.type === "text/csv") {
const formData = new FormData();
formData.append("data", archivo);
try {
const response = await axios.post("/compras/saldos", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
this.getGruposDeCompra();
this.toast({ mensaje: (response.data.message || "Canasta cargada exitosamente") });
} catch (error) {
console.log(error);
this.toast({ mensaje: (error.response?.data?.message || "Hubo errores.") });
}
event.component.cargando = false;
} else {
this.toast("El archivo debe ser .CSV");
event.component.cargando = false;
}
},
toggleSaldosFileDialog() {
this.show_saldos_file_dialog = !this.show_saldos_file_dialog;
},
}, },
computed: { computed: {
...mapState('comisiones', [ ...mapState('comisiones', [

View file

@ -0,0 +1,56 @@
<template>
<div class="file has-name">
<label class="file-label">
<input
class="file-input"
type="file"
name="canasta"
@change="archivoSubido"
/>
<span class="file-cta">
<span class="file-icon">
<i class="fas fa-cloud-upload-alt"></i>
</span>
<span class="file-label">{{ text }}</span>
</span>
<span class="file-name" v-if="cargando">
{{ 'Cargando ' + archivo.nombre }}
</span>
</label>
</div>
</template>
<script>
export default {
name: "InputFileButton",
props: {
text: {
type: String,
required: true,
},
},
data() {
return {
archivo: null,
cargando: false,
};
},
methods: {
archivoSubido(event) {
const archivo = event.target.files[0];
if (archivo) {
this.archivo = { data: archivo, nombre: archivo.name };
this.$emit("archivo-subido", {
component: this,
archivo: archivo
});
this.cargando = true;
}
},
},
};
</script>
<style scoped>
</style>

View file

@ -54,4 +54,5 @@ Route::middleware(['auth', 'role:comision'])->group( function() {
Route::get('/compras/pedidos/pdf', 'ComprasController@pdf')->name('compras.pedidos.pdf'); Route::get('/compras/pedidos/pdf', 'ComprasController@pdf')->name('compras.pedidos.pdf');
Route::get('/compras/canasta/ejemplo', 'ComprasController@descargarCanastaEjemplo')->name('compras.canasta.ejemplo'); Route::get('/compras/canasta/ejemplo', 'ComprasController@descargarCanastaEjemplo')->name('compras.canasta.ejemplo');
Route::post('/compras/canasta', 'ComprasController@cargarCanasta')->name('compras.canasta'); Route::post('/compras/canasta', 'ComprasController@cargarCanasta')->name('compras.canasta');
Route::post('/compras/saldos', 'ComprasController@cargarSaldos')->name('compras.canasta');
}); });