Merge remote-tracking branch 'origin/funcion/refactor-general' into funcion/saldos

# Conflicts:
#    app/Http/Controllers/ComprasController.php
#    resources/js/components/compras/Body.vue
#    routes/web.php
This commit is contained in:
Rodrigo 2025-06-15 13:48:54 -03:00
commit 8d50a29355
24 changed files with 211 additions and 59 deletions

2
.gitignore vendored
View file

@ -13,6 +13,8 @@ yarn-error.log
.idea
/resources/csv/exports/*.csv
/resources/csv/canastas/*.csv
/storage/csv/exports/*.csv
/storage/csv/canastas/*.csv
/public/css/
/public/js/
/public/mix-manifest.json

View file

@ -117,7 +117,8 @@ class GrupoDeCompra extends Model
public function exportarPedidosAPdf()
{
$subpedidos = $this->pedidosAprobados();
PdfHelper::exportarPedidos($this->nombre . '.pdf', $subpedidos);
$fecha = now()->format('Y-m-d');
PdfHelper::exportarPedidos($this->nombre . '-' . $fecha . '.pdf', $subpedidos);
}
function pedidoParaPdf(): array
@ -150,7 +151,8 @@ class GrupoDeCompra extends Model
public static function exportarPedidosBarrialesAPdf()
{
$barrios = GrupoDeCompra::barriosMenosPrueba()->get();
PdfHelper::exportarPedidos('pedidos_por_barrio.pdf', $barrios);
$fecha = now()->format('Y-m-d');
PdfHelper::exportarPedidos('pedidos_por_barrio-' . $fecha . '.pdf', $barrios);
}
static function filaVacia(string $product, int $columns): array
@ -184,7 +186,8 @@ class GrupoDeCompra extends Model
{
$records = $this->generarColumnaCantidades();
CsvHelper::generarCsv('csv/exports/' . $this->nombre . '.csv', $records);
$fecha = now()->format('Y-m-d');
CsvHelper::generarCsv('csv/exports/' . $this->nombre . '-' . $fecha . '.csv', $records);
}
public function generarColumnaCantidades(): array
@ -241,7 +244,8 @@ class GrupoDeCompra extends Model
}
array_splice($records, 0, 0, array($nucleos));
CsvHelper::generarCsv('csv/exports/' . $this->nombre . '-completo.csv', $records);
$fecha = now()->format('Y-m-d');
CsvHelper::generarCsv('csv/exports/' . $this->nombre . '-completo-' . $fecha . '.csv', $records);
}
public function agregarCantidad($pedido, $id, array $records, $fila, int $i): array

View file

@ -7,6 +7,7 @@ use App\CanastaLog;
use DatabaseSeeder;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
class CanastaHelper
@ -19,10 +20,25 @@ class CanastaHelper
const CANASTA_CARGADA = 'Canasta cargada';
const PRODUCTO_TALLE_COLOR = "PTC";
public static function canastaActual() {
$result = [];
$log = CanastaLog::where('descripcion', self::CANASTA_CARGADA)
->orderBy('created_at', 'desc')
->first();
$nombre = str_replace("csv/canastas/", "", $log->path);
$result["nombre"] = str_replace(".csv", "", $nombre);
$result["fecha"] = $log->created_at;
return $result;
}
public static function guardarCanasta($data, $path): string {
if (!File::exists(storage_path('csv/canastas'))) {
File::makeDirectory(storage_path('csv/canastas'), 0755, true);
}
$nombre = $data->getClientOriginalName();
$data->move(resource_path($path), $nombre);
$data->move(storage_path($path), $nombre);
self::log($path . $nombre, self::ARCHIVO_SUBIDO);

View file

@ -2,6 +2,7 @@
namespace App\Helpers;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Iterator;
use League\Csv\CannotInsertRecord;
@ -13,7 +14,7 @@ use League\Csv\Writer;
class CsvHelper
{
public static function getRecords($filePath): Iterator {
$csv = Reader::createFromPath(resource_path($filePath));
$csv = Reader::createFromPath(storage_path($filePath));
try {
$csv->setDelimiter("|");
$csv->setEnclosure("'");
@ -27,8 +28,12 @@ class CsvHelper
public static function generarCsv($filePath, $contenido, $headers = null): void
{
if (!File::exists(storage_path('csv/exports'))) {
File::makeDirectory(storage_path('csv/exports'), 0755, true);
}
try {
$writer = Writer::createFromPath(resource_path($filePath), 'w');
$writer = Writer::createFromPath(storage_path($filePath), 'w');
if ($headers) {
$writer->insertOne($headers);
}

View file

@ -23,14 +23,26 @@ class AdminController extends Controller
public function exportarPedidoACSV(GrupoDeCompra $gdc): BinaryFileResponse
{
$gdc->exportarPedidoEnCSV();
$file = resource_path('csv/exports/'.$gdc->nombre.'.csv');
return response()->download($file);
$pattern = storage_path('csv/exports/'. $gdc->nombre . '-*.csv');
$files = glob($pattern);
usort($files, function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
return response()->download($files[0]);
}
public function exportarPedidoConNucleosACSV(GrupoDeCompra $gdc): BinaryFileResponse
{
$gdc->exportarPedidoConNucleosEnCSV();
$file = resource_path('csv/exports/'.$gdc->nombre.'-completo.csv');
return response()->download($file);
$pattern = storage_path('csv/exports/'.$gdc->nombre.'-completo-*.csv');
$files = glob($pattern);
usort($files, function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
return response()->download($files[0]);
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\CanastaHelper;
use App\Http\Controllers\Controller;
class CanastaController extends Controller
{
public function canastaActual() {
return response()->json(CanastaHelper::canastaActual());
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace App\Http\Controllers;
use App\GrupoDeCompra;
use App\Helpers\CanastaHelper;
use App\Producto;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ComisionesController
{
const CANASTAS_PATH = 'csv/canastas/';
public function show()
{
return view('auth/login');
}
public function descargarPedidos(): BinaryFileResponse
{
Producto::planillaTotales();
$pattern = storage_path('csv/exports/pedidos-por-barrio-*.csv');
$files = glob($pattern);
usort($files, function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
return response()->download($files[0]);
}
public function descargarNotas(): BinaryFileResponse
{
Producto::planillaNotas();
$pattern = storage_path('csv/exports/notas-por-barrio-*.csv');
$files = glob($pattern);
usort($files, function ($a, $b) {
return filemtime($b) <=> filemtime($a);
});
return response()->download($files[0]);
}
public function pdf() {
GrupoDeCompra::exportarPedidosBarrialesAPdf();
}
public function cargarCanasta(Request $request): JsonResponse
{
$request->validate([
'data' => 'required|file|mimes:csv,txt|max:2048',
]);
$nombre = CanastaHelper::guardarCanasta($request->file('data'), self::CANASTAS_PATH);
CanastaHelper::cargarCanasta(self::CANASTAS_PATH . $nombre);
return response()->json([
'message' => 'Canasta cargada exitosamente',
]);
}
public function descargarCanastaEjemplo(): BinaryFileResponse
{
$file = storage_path('csv/productos.csv');
return response()->download($file);
}
}

View file

@ -22,7 +22,7 @@ class RouteController extends Controller
case $admin->id:
return redirect('/admin');
case $comision->id:
return redirect('/compras');
return redirect('/comisiones');
default:
abort(400, 'Rol de usuario invalido');
}

View file

@ -57,8 +57,6 @@ class Kernel extends HttpKernel
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'admin' => \App\Http\Middleware\Admin::class,
'compras' => \App\Http\Middleware\Compras::class,
'role' => \App\Http\Middleware\CheckRole::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,

View file

@ -18,8 +18,8 @@ class Authenticate extends Middleware
$path = $request->path();
if (preg_match('~^admin.*~i', $path))
return route('admin.login');
if (preg_match('~^compras.*~i', $path))
return route('compras.login');
if (preg_match('~^comisiones.*~i', $path))
return route('comisiones.login');
return route('login');
}
}

View file

@ -109,7 +109,8 @@ class Producto extends Model
$planilla[$filaTransporte][] = $cantidad;
}
CsvHelper::generarCsv('csv/exports/pedidos-por-barrio.csv', $planilla, $headers);
$fecha = now()->format('Y-m-d');
CsvHelper::generarCsv('csv/exports/pedidos-por-barrio- ' . $fecha . '.csv', $planilla, $headers);
}
public static function notasPorBarrio(): Collection
@ -146,6 +147,7 @@ class Producto extends Model
$planilla[] = $fila;
}
CsvHelper::generarCsv('csv/exports/notas-por-barrio.csv', $planilla, $headers);
$fecha = now()->format('Y-m-d');
CsvHelper::generarCsv('csv/exports/notas-por-barrio-' . $fecha . '.csv', $planilla, $headers);
}
}

View file

@ -1,14 +1,17 @@
<script>
import NavBar from "./comunes/NavBar.vue";
import { mapActions, mapState } from "vuex";
import ComisionesBody from "./comisiones/Body.vue";
import AdminBody from "./admin/Body.vue";
import PedidosBody from "./pedidos/Body.vue";
export default {
name: 'Main',
components: { NavBar },
components: { ComisionesBody, AdminBody, PedidosBody, NavBar },
computed: {
...mapState('login',["rol"]),
...mapState("login", ["rol"]),
},
methods: {
...mapActions('login',["getRol"]),
...mapActions("login", ["getRol"]),
},
async mounted() {
await this.getRol();
@ -21,7 +24,7 @@ export default {
<nav-bar></nav-bar>
<pedidos-body v-if="rol === 'barrio'"></pedidos-body>
<admin-body v-if="rol === 'admin_barrio'"></admin-body>
<compras-body v-if="rol === 'comision'"></compras-body>
<comisiones-body v-if="rol === 'comision'"></comisiones-body>
</div>
</template>

View file

@ -25,6 +25,7 @@ import DropdownDescargar from "./DropdownDescargar.vue";
import TablaPedidos from "./TablaPedidos.vue";
import { mapActions, mapGetters } from "vuex";
export default {
name: "AdminBody",
components: {
CaracteristicasOpcionales,
TabsSecciones,

View file

@ -1,16 +1,16 @@
<template>
<div class="block ml-3 mr-3 is-max-widescreen is-max-desktop">
<comunes-tabs-secciones :tabs="tabs" :tabInicial="tabActiva"></comunes-tabs-secciones>
<div class="block pb-6" id="pedidos-compras-seccion"
:class="seccionActiva === 'pedidos-compras-seccion' ? 'is-active' : 'is-hidden'">
<div class="block" id="pedidos-compras-tabla-y-dropdown">
<div class="block pb-6" id="pedidos-comisiones-seccion"
:class="seccionActiva === 'pedidos-comisiones-seccion' ? 'is-active' : 'is-hidden'">
<div class="block" id="pedidos-comisiones-tabla-y-dropdown">
<dropdown-descargar>
</dropdown-descargar>
</div>
</div>
<div class="block pb-6" id="canasta-compras-seccion"
:class="seccionActiva === 'canasta-compras-seccion' ? 'is-active' : 'is-hidden'">
<div class="block" id="canasta-compras-seccion">
<div class="block pb-6" id="canasta-comisiones-seccion"
:class="seccionActiva === 'canasta-comisiones-seccion' ? 'is-active' : 'is-hidden'">
<div class="block" id="canasta-comisiones-seccion">
<article class="message is-warning">
<div class="message-header">
<p>Formato de la canasta</p>
@ -25,7 +25,7 @@
<li> No puede haber "enters" en ninguna celda </li>
<li> El bono de transporte debe tener tipo 'T' </li>
</ul>
<a class="has-text-info" href="/compras/canasta/ejemplo">Planilla de ejemplo.</a>
<a class="has-text-info" href="/comisiones/canasta/ejemplo">Planilla de ejemplo.</a>
<article class="message is-danger mt-2">
<div class="message-body">
<div class="content">
@ -43,10 +43,10 @@
</div>
<div
class="block pb-6"
id="saldos-compras-seccion"
:class="seccionActiva === 'saldos-compras-seccion' ? 'is-active' : 'is-hidden'"
id="saldos-comisiones-seccion"
:class="seccionActiva === 'saldos-comisiones-seccion' ? 'is-active' : 'is-hidden'"
>
<div class="block" id="saldos-compras-seccion">
<div class="block" id="saldos-comisiones-seccion">
<article class="message is-warning">
<div class="message-header">
<p>
@ -119,6 +119,7 @@ import InputFileButton from "../comunes/InputFileButton.vue";
import { mapActions, mapState } from "vuex";
export default {
name: "ComisionesBody",
components: {
TabsSecciones,
DropdownDescargar,
@ -128,12 +129,12 @@ export default {
data() {
return {
tabs: [
{ id: "pedidos-compras", nombre: "Pedidos" },
{ id: "canasta-compras", nombre: "Canasta" },
{ id: "saldos-compras", nombre: "Saldos" },
{ id: "pedidos-comisiones", nombre: "Pedidos" },
{ id: "canasta-comisiones", nombre: "Canasta" },
{ id: "saldos-comisiones", nombre: "Saldos" },
],
tabActiva: "pedidos-compras",
seccionActiva: "pedidos-compras-seccion",
tabActiva: "pedidos-comisiones",
seccionActiva: "pedidos-comisiones-seccion",
archivo: undefined,
saldo_modificado: {},
show_saldos_file_dialog: false,
@ -175,7 +176,7 @@ export default {
const formData = new FormData();
formData.append("data", archivo);
try {
const response = await axios.post("/compras/saldos", formData, {
const response = await axios.post("/comisiones/saldos", formData, {
headers: {
"Content-Type": "multipart/form-data",
},

View file

@ -45,7 +45,7 @@ export default {
try {
this.cargando = true;
const response = await axios.post("/compras/canasta", formData, {
const response = await axios.post("/comisiones/canasta", formData, {
headers: {
"Content-Type": "multipart/form-data",
},

View file

@ -14,13 +14,13 @@
</div>
<div class="dropdown-menu" id="dropdown-menu" role="menu">
<div class="dropdown-content">
<a href="/compras/pedidos/descargar" class="dropdown-item">
<a href="/comisiones/pedidos/descargar" class="dropdown-item">
Pedidos por barrio
</a>
<a href="/compras/pedidos/notas" class="dropdown-item">
<a href="/comisiones/pedidos/notas" class="dropdown-item">
Notas por barrio
</a>
<a href="/compras/pedidos/pdf" class="dropdown-item">
<a href="/comisiones/pedidos/pdf" class="dropdown-item">
Pedidos por barrio en pdf
</a>
</div>

View file

@ -4,9 +4,12 @@
<a class="navbar-item" href="https://mps.org.uy">
<img src="/assets/logoMPS.png" height="28">
</a>
<div class="navbar-item" id="datos-pedido" v-if="pedidoDefinido">
<p class="hide-below-1024">
{{ `Núcleo: ${nombre} - Barrio: ${grupo_de_compra.nombre}` }}
<div class="navbar-item hide-below-1024">
<p>
{{ `Canasta actual: ${nombreCanasta} - Actualizada: ${fechaCanasta}`}}
</p>
<p class="ml-2" v-if="pedidoDefinido">
{{ `- Núcleo: ${nombre} - Barrio: ${grupo_de_compra.nombre}` }}
</p>
</div>
<chismosa-dropdown
@ -58,16 +61,20 @@ export default {
return {
burgerActiva: false,
searchString: "",
nombreCanasta: "",
fechaCanasta: "",
}
},
computed: {
...mapGetters('pedido', ["pedidoDefinido"]),
...mapState('pedido', ["nombre"]),
...mapState('pedido', ["grupo_de_compra"]),
...mapState('ui', ["canasta_actual"])
},
methods: {
...mapActions('productos', ["filtrarProductos"]),
...mapMutations('ui', ["addMiga", "popUltimaBusqueda"]),
...mapActions('ui', ["getCanastaActual"]),
toggleBurger() {
this.burgerActiva = !this.burgerActiva
},
@ -79,6 +86,12 @@ export default {
this.addMiga({ nombre: this.searchString });
}
},
async mounted() {
await this.getCanastaActual();
this.fechaCanasta = new Date(this.canasta_actual.fecha)
.toLocaleDateString('es-UY');
this.nombreCanasta = this.canasta_actual.nombre;
}
};
</script>

View file

@ -14,7 +14,7 @@ export default {
<template>
<div>
<user-login v-if="urlRol === 'compras'"></user-login>
<user-login v-if="urlRol === 'comisiones'"></user-login>
<barrio-login v-else></barrio-login>
</div>
</template>

View file

@ -25,6 +25,7 @@ import NavMigas from "./NavMigas.vue";
import Chismosa from "./Chismosa.vue";
export default {
name: "PedidosBody",
components: { Chismosa, NavMigas, CartelPedidoAprobado, PedidoSelect, Canasta },
computed: {
...mapGetters('pedido', ["pedidoDefinido"]),

View file

@ -58,7 +58,7 @@ const getters = {
ayuda: "Si no la sabés, consultá a la comisión informática",
label: "Seleccioná tu región"
};
case 'compras':
case 'comisiones':
return {
titulo: "Comisiones MPS",
subtitlo: "página de comisiones",
@ -87,7 +87,7 @@ const getters = {
texto: "has-text-white",
botones: "is-warning",
};
case 'compras':
case 'comisiones':
return {
fondo: "has-background-warning",
texto: "",
@ -109,9 +109,9 @@ const getters = {
case 'admin':
return [
{ nombre: "Pedidos", href: "/" },
{ nombre: "Compras", href: "/compras" }
{ nombre: "Comisiones", href: "/comisiones" }
];
case 'compras':
case 'comisiones':
return [
{ nombre: "Pedidos", href: "/" },
{ nombre: "Administración", href: "/admin" }
@ -119,7 +119,7 @@ const getters = {
case 'pedido':
return [
{ nombre: "Administración", href: "/admin" },
{ nombre: "Compras", href: "/compras" }
{ nombre: "Comisiones", href: "/comisiones" }
];
default:
throw new Error("Url inválida");

View file

@ -2,9 +2,13 @@ const state = {
show_chismosa: false,
show_devoluciones: false,
migas: [{ nombre: 'Pedidos', action: 'pedido/resetear' }],
canasta_actual: null,
};
const mutations = {
setCanastaActual(state, { canasta }) {
state.canasta_actual = canasta;
},
toggleChismosa(state) {
state.show_chismosa = !state.show_chismosa;
},
@ -25,6 +29,10 @@ const mutations = {
};
const actions = {
async getCanastaActual({ commit }) {
const response = await axios.get('/api/canasta-actual');
commit("setCanastaActual", { canasta: response.data });
},
clickMiga({ dispatch }, { miga }) {
let dropWhile = (array, pred) => {
let result = array.slice(0);

View file

@ -17,6 +17,8 @@ Route::middleware('api')->group(function() {
Route::get('/regiones', 'Api\GrupoDeCompraController@regiones');
Route::get('/regiones/{region}', 'Api\GrupoDeCompraController@region');
Route::get('/canasta-actual', 'Api\CanastaController@canastaActual');
Route::prefix('grupos-de-compra')->group(function() {
Route::get('/', 'Api\GrupoDeCompraController@index');
Route::get('/{grupoDeCompra}', 'Api\GrupoDeCompraController@show');

View file

@ -45,14 +45,14 @@ Route::middleware(['auth', 'role:admin_barrio'])->group(function () {
Route::get('/admin/exportar-pedido-con-nucleos-a-csv/{gdc}', 'AdminController@exportarPedidoConNucleosACSV');
});
Route::get('/compras/login', 'ComprasController@show')->name('compras.login');
Route::get('/comisiones/login', 'ComisionesController@show')->name('comisiones.login');
Route::middleware(['auth', 'role:comision'])->group( function() {
Route::get('/compras', 'RouteController@main')->name('compras');
Route::get('/compras/pedidos/descargar', 'ComprasController@descargarPedidos')->name('compras.pedidos.descargar');
Route::get('/compras/pedidos/notas', 'ComprasController@descargarNotas')->name('compras.pedidos.descargar');
Route::get('/compras/pedidos/pdf', 'ComprasController@pdf')->name('compras.pedidos.pdf');
Route::get('/compras/canasta/ejemplo', 'ComprasController@descargarCanastaEjemplo')->name('compras.canasta.ejemplo');
Route::post('/compras/canasta', 'ComprasController@cargarCanasta')->name('compras.canasta');
Route::post('/compras/saldos', 'ComprasController@cargarSaldos')->name('compras.canasta');
Route::get('/comisiones', 'RouteController@main')->name('comisiones');
Route::get('/comisiones/pedidos/descargar', 'ComisionesController@descargarPedidos')->name('comisiones.pedidos.descargar');
Route::get('/comisiones/pedidos/notas', 'ComisionesController@descargarNotas')->name('comisiones.pedidos.descargar');
Route::get('/comisiones/pedidos/pdf', 'ComisionesController@pdf')->name('comisiones.pedidos.pdf');
Route::get('/comisiones/canasta/ejemplo', 'ComisionesController@descargarCanastaEjemplo')->name('comisiones.canasta.ejemplo');
Route::post('/comisiones/canasta', 'ComisionesController@cargarCanasta')->name('comisiones.canasta');
Route::post('/comisiones/saldos', 'ComprasController@cargarSaldos')->name('compras.canasta');
});

1
run_watch.sh Executable file
View file

@ -0,0 +1 @@
sudo docker compose exec app npm run watch