mirror of
https://github.com/LaCasemate/fab-manager.git
synced 2025-02-20 14:54:15 +01:00
fablab store page
This commit is contained in:
parent
1cef45e3d7
commit
16288ae2bd
@ -4,13 +4,15 @@
|
||||
# ProductCategorys are used to group products
|
||||
class API::ProductCategoriesController < API::ApiController
|
||||
before_action :authenticate_user!, except: :index
|
||||
before_action :set_product_category, only: %i[show update destroy position]
|
||||
before_action :set_product_category, only: %i[update destroy position]
|
||||
|
||||
def index
|
||||
@product_categories = ProductCategoryService.list
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@product_category = ProductCategory.friendly.find(params[:id])
|
||||
end
|
||||
|
||||
def create
|
||||
authorize ProductCategory
|
||||
|
@ -4,13 +4,15 @@
|
||||
# Products are used in store
|
||||
class API::ProductsController < API::ApiController
|
||||
before_action :authenticate_user!, except: %i[index show]
|
||||
before_action :set_product, only: %i[show update destroy]
|
||||
before_action :set_product, only: %i[update destroy]
|
||||
|
||||
def index
|
||||
@products = ProductService.list
|
||||
@products = ProductService.list(params)
|
||||
end
|
||||
|
||||
def show; end
|
||||
def show
|
||||
@product = Product.includes(:product_images, :product_files).friendly.find(params[:id])
|
||||
end
|
||||
|
||||
def create
|
||||
authorize Product
|
||||
|
@ -1,15 +1,16 @@
|
||||
import apiClient from './clients/api-client';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { serialize } from 'object-to-formdata';
|
||||
import { Product } from '../models/product';
|
||||
import { Product, ProductIndexFilter } from '../models/product';
|
||||
import ApiLib from '../lib/api';
|
||||
|
||||
export default class ProductAPI {
|
||||
static async index (): Promise<Array<Product>> {
|
||||
const res: AxiosResponse<Array<Product>> = await apiClient.get('/api/products');
|
||||
static async index (filters?: ProductIndexFilter): Promise<Array<Product>> {
|
||||
const res: AxiosResponse<Array<Product>> = await apiClient.get(`/api/products${ApiLib.filtersToQuery(filters)}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
||||
static async get (id: number): Promise<Product> {
|
||||
static async get (id: number | string): Promise<Product> {
|
||||
const res: AxiosResponse<Product> = await apiClient.get(`/api/products/${id}`);
|
||||
return res?.data;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useForm, useWatch, Path } from 'react-hook-form';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import slugify from 'slugify';
|
||||
import _ from 'lodash';
|
||||
@ -11,7 +11,7 @@ import { FormSelect } from '../form/form-select';
|
||||
import { FormChecklist } from '../form/form-checklist';
|
||||
import { FormRichText } from '../form/form-rich-text';
|
||||
import { FormFileUpload } from '../form/form-file-upload';
|
||||
import { FormImageUpload, ImageType } from '../form/form-image-upload';
|
||||
import { FormImageUpload } from '../form/form-image-upload';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { FabAlert } from '../base/fab-alert';
|
||||
import ProductCategoryAPI from '../../api/product-category';
|
||||
|
@ -0,0 +1,63 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import _ from 'lodash';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Product } from '../../models/product';
|
||||
import FormatLib from '../../lib/format';
|
||||
|
||||
interface StoreProductItemProps {
|
||||
product: Product,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product item in store
|
||||
*/
|
||||
export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
/**
|
||||
* Return main image of Product, if the product has not any image, show default image
|
||||
*/
|
||||
const productImageUrl = (product: Product) => {
|
||||
const productImage = _.find(product.product_images_attributes, { is_main: true });
|
||||
if (productImage) {
|
||||
return productImage.attachment_url;
|
||||
}
|
||||
return 'https://via.placeholder.com/300';
|
||||
};
|
||||
|
||||
/**
|
||||
* Return product's stock status
|
||||
*/
|
||||
const productStockStatus = (product: Product) => {
|
||||
if (product.low_stock_threshold && product.stock.external < product.low_stock_threshold) {
|
||||
return <span>{t('app.public.store_product_item.limited_stock')}</span>;
|
||||
}
|
||||
return <span>{t('app.public.store_product_item.available')}</span>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Goto show product page
|
||||
*/
|
||||
const showProduct = (product: Product): void => {
|
||||
window.location.href = `/#!/store/p/${product.slug}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="store-product-item" onClick={() => showProduct(product)}>
|
||||
<div className='itemInfo-image'>
|
||||
<img src={productImageUrl(product)} alt='' className='itemInfo-thumbnail' />
|
||||
</div>
|
||||
<p className="itemInfo-name">{product.name}</p>
|
||||
<div className=''>
|
||||
<span>
|
||||
<div>{FormatLib.price(product.amount)}</div>
|
||||
{productStockStatus(product)}
|
||||
</span>
|
||||
<FabButton className='edit-btn'>
|
||||
<i className="fas fa-cart-arrow-down" /> {t('app.public.store_product_item.add')}
|
||||
</FabButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import _ from 'lodash';
|
||||
import { Product } from '../../models/product';
|
||||
import ProductAPI from '../../api/product';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface StoreProductProps {
|
||||
productSlug: string,
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows a product
|
||||
*/
|
||||
export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const [product, setProduct] = useState<Product>();
|
||||
|
||||
useEffect(() => {
|
||||
ProductAPI.get(productSlug).then(data => {
|
||||
setProduct(data);
|
||||
}).catch(() => {
|
||||
onError(t('app.public.store_product.unexpected_error_occurred'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Return main image of Product, if the product has not any image, show default image
|
||||
*/
|
||||
const productImageUrl = (product: Product) => {
|
||||
const productImage = _.find(product.product_images_attributes, { is_main: true });
|
||||
if (productImage) {
|
||||
return productImage.attachment_url;
|
||||
}
|
||||
return 'https://via.placeholder.com/300';
|
||||
};
|
||||
|
||||
if (product) {
|
||||
return (
|
||||
<div className="store-product">
|
||||
<img src={productImageUrl(product)} alt='' className='itemInfo-thumbnail' />
|
||||
<p className="itemInfo-name">{product.name}</p>
|
||||
<div dangerouslySetInnerHTML={{ __html: product.description }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const StoreProductWrapper: React.FC<StoreProductProps> = ({ productSlug, onError }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<StoreProduct productSlug={productSlug} onError={onError} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('storeProduct', react2angular(StoreProductWrapper, ['productSlug', 'onError']));
|
88
app/frontend/src/javascript/components/store/store.tsx
Normal file
88
app/frontend/src/javascript/components/store/store.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { react2angular } from 'react2angular';
|
||||
import { Loader } from '../base/loader';
|
||||
import { IApplication } from '../../models/application';
|
||||
import { FabButton } from '../base/fab-button';
|
||||
import { Product } from '../../models/product';
|
||||
import ProductAPI from '../../api/product';
|
||||
import { StoreProductItem } from './store-product-item';
|
||||
|
||||
declare const Application: IApplication;
|
||||
|
||||
interface StoreProps {
|
||||
onError: (message: string) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* This component shows public store
|
||||
*/
|
||||
const Store: React.FC<StoreProps> = ({ onError }) => {
|
||||
const { t } = useTranslation('public');
|
||||
|
||||
const [products, setProducts] = useState<Array<Product>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
ProductAPI.index({ is_active: true }).then(data => {
|
||||
setProducts(data);
|
||||
}).catch(() => {
|
||||
onError(t('app.public.store.unexpected_error_occurred'));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="store">
|
||||
<div className='layout'>
|
||||
<div className='store-filters span-3'>
|
||||
<header>
|
||||
<h3>Filtrer</h3>
|
||||
<div className='grpBtn'>
|
||||
<FabButton className="is-black">Clear</FabButton>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
<div className='store-products-list span-7'>
|
||||
<div className='status'>
|
||||
<div className='count'>
|
||||
<p>Result count: <span>{products.length}</span></p>
|
||||
</div>
|
||||
<div className="">
|
||||
<div className='sort'>
|
||||
<p>Display options:</p>
|
||||
</div>
|
||||
<div className='visibility'>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='features'>
|
||||
<div className='features-item'>
|
||||
<p>feature name</p>
|
||||
<button><i className="fa fa-times" /></button>
|
||||
</div>
|
||||
<div className='features-item'>
|
||||
<p>long feature name</p>
|
||||
<button><i className="fa fa-times" /></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="products">
|
||||
{products.map((product) => (
|
||||
<StoreProductItem key={product.id} product={product} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StoreWrapper: React.FC<StoreProps> = ({ onError }) => {
|
||||
return (
|
||||
<Loader>
|
||||
<Store onError={onError} />
|
||||
</Loader>
|
||||
);
|
||||
};
|
||||
|
||||
Application.Components.component('store', react2angular(StoreWrapper, ['onError']));
|
@ -53,6 +53,12 @@ Application.Controllers.controller('MainNavController', ['$scope', 'settingsProm
|
||||
linkIcon: 'tags',
|
||||
class: 'reserve-event-link'
|
||||
},
|
||||
{
|
||||
state: 'app.public.store',
|
||||
linkText: 'app.public.common.fablab_store',
|
||||
linkIcon: 'cart-plus',
|
||||
class: 'store-link'
|
||||
},
|
||||
{ class: 'menu-spacer' },
|
||||
{
|
||||
state: 'app.public.projects_list',
|
||||
|
42
app/frontend/src/javascript/controllers/products.js
Normal file
42
app/frontend/src/javascript/controllers/products.js
Normal file
@ -0,0 +1,42 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('ShowProductController', ['$scope', 'CSRF', 'growl', '$transition$',
|
||||
function ($scope, CSRF, growl, $transition$) {
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
$scope.productSlug = $transition$.params().slug;
|
||||
|
||||
/**
|
||||
* Callback triggered in case of error
|
||||
*/
|
||||
$scope.onError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered in case of success
|
||||
*/
|
||||
$scope.onSuccess = (message) => {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
*/
|
||||
const initialize = function () {
|
||||
// set the authenticity tokens in the forms
|
||||
CSRF.setMetaTags();
|
||||
};
|
||||
|
||||
// init the controller (call at the end !)
|
||||
return initialize();
|
||||
}
|
||||
|
||||
]);
|
41
app/frontend/src/javascript/controllers/store.js
Normal file
41
app/frontend/src/javascript/controllers/store.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* eslint-disable
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
Application.Controllers.controller('StoreController', ['$scope', 'CSRF', 'growl', '$state',
|
||||
function ($scope, CSRF, growl, $state) {
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/* PUBLIC SCOPE */
|
||||
|
||||
/**
|
||||
* Callback triggered in case of error
|
||||
*/
|
||||
$scope.onError = (message) => {
|
||||
growl.error(message);
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback triggered in case of success
|
||||
*/
|
||||
$scope.onSuccess = (message) => {
|
||||
growl.success(message);
|
||||
};
|
||||
|
||||
/* PRIVATE SCOPE */
|
||||
|
||||
/**
|
||||
* Kind of constructor: these actions will be realized first when the controller is loaded
|
||||
*/
|
||||
const initialize = function () {
|
||||
// set the authenticity tokens in the forms
|
||||
CSRF.setMetaTags();
|
||||
};
|
||||
|
||||
// init the controller (call at the end !)
|
||||
return initialize();
|
||||
}
|
||||
|
||||
]);
|
@ -1,4 +1,9 @@
|
||||
import { TDateISO } from '../typings/date-iso';
|
||||
import { ApiFilter } from './api';
|
||||
|
||||
export interface ProductIndexFilter extends ApiFilter {
|
||||
is_active: boolean,
|
||||
}
|
||||
|
||||
export enum StockType {
|
||||
internal = 'internal',
|
||||
|
@ -600,6 +600,28 @@ angular.module('application.router', ['ui.router'])
|
||||
}
|
||||
})
|
||||
|
||||
// store
|
||||
.state('app.public.store', {
|
||||
url: '/store',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/store/index.html',
|
||||
controller: 'StoreController'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// show product
|
||||
.state('app.public.product_show', {
|
||||
url: '/store/p/:slug',
|
||||
views: {
|
||||
'main@': {
|
||||
templateUrl: '/products/show.html',
|
||||
controller: 'ShowProductController'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// --- namespace /admin/... ---
|
||||
// calendar
|
||||
.state('app.admin.calendar', {
|
||||
|
@ -92,6 +92,7 @@
|
||||
@import "modules/store/manage-product-category";
|
||||
@import "modules/store/product-categories";
|
||||
@import "modules/store/products";
|
||||
@import "modules/store/store";
|
||||
@import "modules/subscriptions/free-extend-modal";
|
||||
@import "modules/subscriptions/renew-modal";
|
||||
@import "modules/supporting-documents/supporting-documents-files";
|
||||
|
170
app/frontend/src/stylesheets/modules/store/store.scss
Normal file
170
app/frontend/src/stylesheets/modules/store/store.scss
Normal file
@ -0,0 +1,170 @@
|
||||
.store {
|
||||
margin: 0 auto;
|
||||
padding-bottom: 6rem;
|
||||
|
||||
.back-btn {
|
||||
margin: 2.4rem 0;
|
||||
padding: 0.4rem 0.8rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: var(--gray-soft-darkest);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--gray-soft-lightest);
|
||||
i { margin-right: 0.8rem; }
|
||||
|
||||
&:hover {
|
||||
background-color: var(--gray-hard-lightest);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 2.4rem 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
.grpBtn {
|
||||
display: flex;
|
||||
& > *:not(:first-child) { margin-left: 2.4rem; }
|
||||
}
|
||||
h2 {
|
||||
margin: 0;
|
||||
@include title-lg;
|
||||
color: var(--gray-hard-darkest) !important;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
@include text-lg(600);
|
||||
color: var(--gray-hard-darkest) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 0 3.2rem;
|
||||
.span-7 { flex: 1 1 70%; }
|
||||
.span-3 { flex: 1 1 30%; }
|
||||
}
|
||||
|
||||
.main-action-btn {
|
||||
background-color: var(--main);
|
||||
color: var(--gray-soft-lightest);
|
||||
border: none;
|
||||
&:hover { opacity: 0.75; }
|
||||
}
|
||||
|
||||
.main-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
& > *:not(:first-child) {
|
||||
margin-left: 1.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.store {
|
||||
max-width: 1600px;
|
||||
|
||||
.layout {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&-filters {
|
||||
}
|
||||
|
||||
&-products-list {
|
||||
.products {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 1.6rem 2.4rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: var(--gray-soft);
|
||||
border-radius: var(--border-radius);
|
||||
p { margin: 0; }
|
||||
.count {
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include text-sm;
|
||||
span {
|
||||
margin-left: 1.6rem;
|
||||
@include text-lg(600);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.features {
|
||||
margin: 2.4rem 0 1.6rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 1.6rem 2.4rem;
|
||||
&-item {
|
||||
padding-left: 1.6rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--information-light);
|
||||
border-radius: 100px;
|
||||
color: var(--information-dark);
|
||||
p { margin: 0; }
|
||||
button {
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
background: none;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-product-item {
|
||||
padding: 1rem 1.8rem;
|
||||
border: 1px solid var(--gray-soft-dark);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft-lightest);
|
||||
|
||||
margin-right: 1.6rem;
|
||||
|
||||
.itemInfo-image {
|
||||
align-items: center;
|
||||
|
||||
img {
|
||||
width: 19.8rem;
|
||||
height: 14.8rem;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--gray-soft);
|
||||
}
|
||||
}
|
||||
.itemInfo-name {
|
||||
margin: 1rem 0;
|
||||
@include text-base;
|
||||
font-weight: 600;
|
||||
color: var(--gray-hard-darkest);
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.manage {
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
border-radius: var(--border-radius-sm);
|
||||
button {
|
||||
@include btn;
|
||||
border-radius: 0;
|
||||
color: var(--gray-soft-lightest);
|
||||
&:hover { opacity: 0.75; }
|
||||
}
|
||||
.edit-btn {background: var(--gray-hard-darkest) }
|
||||
.delete-btn {background: var(--error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
24
app/frontend/templates/products/show.html
Normal file
24
app/frontend/templates/products/show.html
Normal file
@ -0,0 +1,24 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="m-lg">
|
||||
<store-product product-slug="productSlug" on-error="onError" on-success="onSuccess" />
|
||||
</section>
|
24
app/frontend/templates/store/index.html
Normal file
24
app/frontend/templates/store/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<section class="heading b-b">
|
||||
<div class="row no-gutter">
|
||||
<div class="col-xs-2 col-sm-2 col-md-1">
|
||||
<section class="heading-btn">
|
||||
<a ng-click="backPrevLocation($event)"><i class="fas fa-long-arrow-alt-left "></i></a>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-xs-10 col-sm-10 col-md-8 b-l b-r-md">
|
||||
<section class="heading-title">
|
||||
<h1 translate>{{ 'app.public.store.fablab_store' }}</h1>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="col-xs-12 col-sm-12 col-md-3 b-t hide-b-md">
|
||||
<section class="heading-actions wrapper">
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="m-lg">
|
||||
<store on-error="onError" on-success="onSuccess" />
|
||||
</section>
|
@ -2,6 +2,9 @@
|
||||
|
||||
# Product is a model for the merchandise hold information of product in store
|
||||
class Product < ApplicationRecord
|
||||
extend FriendlyId
|
||||
friendly_id :name, use: :slugged
|
||||
|
||||
belongs_to :product_category
|
||||
|
||||
has_and_belongs_to_many :machines
|
||||
@ -15,6 +18,7 @@ class Product < ApplicationRecord
|
||||
has_many :product_stock_movements, dependent: :destroy
|
||||
accepts_nested_attributes_for :product_stock_movements, allow_destroy: true, reject_if: :all_blank
|
||||
|
||||
validates :name, :slug, presence: true
|
||||
validates :amount, numericality: { greater_than: 0, allow_nil: true }
|
||||
|
||||
scope :active, -> { where(is_active: true) }
|
||||
|
@ -2,8 +2,13 @@
|
||||
|
||||
# Provides methods for Product
|
||||
class ProductService
|
||||
def self.list
|
||||
Product.all
|
||||
def self.list(filters)
|
||||
products = Product.includes(:product_images)
|
||||
if filters[:is_active].present?
|
||||
state = filters[:disabled] == 'false' ? [nil, false] : true
|
||||
products = products.where(is_active: state)
|
||||
end
|
||||
products
|
||||
end
|
||||
|
||||
# amount params multiplied by hundred
|
||||
|
@ -1,7 +1,8 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.extract! product, :id, :name, :slug, :sku, :description, :is_active, :product_category_id, :quantity_min, :stock, :low_stock_alert,
|
||||
json.extract! product, :id, :name, :slug, :sku, :is_active, :product_category_id, :quantity_min, :stock, :low_stock_alert,
|
||||
:low_stock_threshold, :machine_ids
|
||||
json.description sanitize(product.description)
|
||||
json.amount product.amount / 100.0 if product.amount.present?
|
||||
json.product_files_attributes product.product_files do |f|
|
||||
json.id f.id
|
||||
|
@ -1,5 +1,13 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
json.array! @products do |product|
|
||||
json.partial! 'api/products/product', product: product
|
||||
json.extract! product, :id, :name, :slug, :sku, :is_active, :product_category_id, :quantity_min, :stock, :machine_ids,
|
||||
:low_stock_threshold
|
||||
json.amount product.amount / 100.0 if product.amount.present?
|
||||
json.product_images_attributes product.product_images do |f|
|
||||
json.id f.id
|
||||
json.attachment_name f.attachment_identifier
|
||||
json.attachment_url f.attachment_url
|
||||
json.is_main f.is_main
|
||||
end
|
||||
end
|
||||
|
@ -43,6 +43,7 @@ en:
|
||||
projects_gallery: "Projects gallery"
|
||||
subscriptions: "Subscriptions"
|
||||
public_calendar: "Calendar"
|
||||
fablab_store: "Fablab Store"
|
||||
#left menu (admin)
|
||||
trainings_monitoring: "Trainings"
|
||||
manage_the_calendar: "Calendar"
|
||||
@ -373,6 +374,16 @@ en:
|
||||
characteristics: "Characteristics"
|
||||
files_to_download: "Files to download"
|
||||
projects_using_the_space: "Projects using the space"
|
||||
#public store
|
||||
store:
|
||||
fablab_store: "FabLab Store"
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
store_product_item:
|
||||
available: "Available"
|
||||
limited_stock: "Limited stock"
|
||||
add: "Add"
|
||||
store_product:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
tour:
|
||||
conclusion:
|
||||
title: "Thank you for your attention"
|
||||
|
@ -43,6 +43,7 @@ fr:
|
||||
projects_gallery: "Galerie de projets"
|
||||
subscriptions: "Abonnements"
|
||||
public_calendar: "Agenda"
|
||||
fablab_store: "Boutique Fablab"
|
||||
#left menu (admin)
|
||||
trainings_monitoring: "Formations"
|
||||
manage_the_calendar: "Agenda"
|
||||
@ -373,6 +374,16 @@ fr:
|
||||
characteristics: "Caractéristiques"
|
||||
files_to_download: "Fichiers à télécharger"
|
||||
projects_using_the_space: "Projets utilisant l'espace"
|
||||
#public store
|
||||
store:
|
||||
fablab_store: "Boutique FabLab"
|
||||
unexpected_error_occurred: "Une erreur inattendue s'est produite. Veuillez réessayer ultérieurement."
|
||||
store_product_item:
|
||||
available: "Disponible"
|
||||
limited_stock: "Stock limité"
|
||||
add: "Ajouter"
|
||||
store_product:
|
||||
unexpected_error_occurred: "An unexpected error occurred. Please try again later."
|
||||
tour:
|
||||
conclusion:
|
||||
title: "Merci de votre attention"
|
||||
|
Loading…
x
Reference in New Issue
Block a user