diff --git a/app/frontend/src/javascript/api/product.ts b/app/frontend/src/javascript/api/product.ts new file mode 100644 index 000000000..edb434c95 --- /dev/null +++ b/app/frontend/src/javascript/api/product.ts @@ -0,0 +1,30 @@ +import apiClient from './clients/api-client'; +import { AxiosResponse } from 'axios'; +import { Product } from '../models/product'; + +export default class ProductAPI { + static async index (): Promise> { + const res: AxiosResponse> = await apiClient.get('/api/products'); + return res?.data; + } + + static async get (id: number): Promise { + const res: AxiosResponse = await apiClient.get(`/api/products/${id}`); + return res?.data; + } + + static async create (product: Product): Promise { + const res: AxiosResponse = await apiClient.post('/api/products', { product }); + return res?.data; + } + + static async update (product: Product): Promise { + const res: AxiosResponse = await apiClient.patch(`/api/products/${product.id}`, { product }); + return res?.data; + } + + static async destroy (productId: number): Promise { + const res: AxiosResponse = await apiClient.delete(`/api/products/${productId}`); + return res?.data; + } +} diff --git a/app/frontend/src/javascript/components/store/products-list.tsx b/app/frontend/src/javascript/components/store/products-list.tsx new file mode 100644 index 000000000..60f05cd96 --- /dev/null +++ b/app/frontend/src/javascript/components/store/products-list.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { FabButton } from '../base/fab-button'; +import { Product } from '../../models/product'; + +interface ProductsListProps { + products: Array, + onEdit: (product: Product) => void, + onDelete: (productId: number) => void, +} + +/** + * This component shows a list of all Products + */ +export const ProductsList: React.FC = ({ products, onEdit, onDelete }) => { + /** + * Init the process of editing the given product + */ + const editProduct = (product: Product): () => void => { + return (): void => { + onEdit(product); + }; + }; + + /** + * Init the process of delete the given product + */ + const deleteProduct = (productId: number): () => void => { + return (): void => { + onDelete(productId); + }; + }; + + return ( +
+ {products.map((product) => ( +
+ {product.name} +
+ + + + + + +
+
+ ))} +
+ ); +}; diff --git a/app/frontend/src/javascript/components/store/products.tsx b/app/frontend/src/javascript/components/store/products.tsx new file mode 100644 index 000000000..09c7e0912 --- /dev/null +++ b/app/frontend/src/javascript/components/store/products.tsx @@ -0,0 +1,77 @@ +import React, { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { react2angular } from 'react2angular'; +import { HtmlTranslate } from '../base/html-translate'; +import { Loader } from '../base/loader'; +import { IApplication } from '../../models/application'; +import { FabAlert } from '../base/fab-alert'; +import { FabButton } from '../base/fab-button'; +import { ProductsList } from './products-list'; +import { Product } from '../../models/product'; +import ProductAPI from '../../api/product'; + +declare const Application: IApplication; + +interface ProductsProps { + onSuccess: (message: string) => void, + onError: (message: string) => void, +} + +/** + * This component shows all Products and filter + */ +const Products: React.FC = ({ onSuccess, onError }) => { + const { t } = useTranslation('admin'); + + const [products, setProducts] = useState>([]); + const [product, setProduct] = useState(null); + + useEffect(() => { + ProductAPI.index().then(data => { + setProducts(data); + }); + }, []); + + /** + * Open edit the product modal + */ + const editProduct = (product: Product) => { + setProduct(product); + }; + + /** + * Delete a product + */ + const deleteProduct = async (productId: number): Promise => { + try { + await ProductAPI.destroy(productId); + const data = await ProductAPI.index(); + setProducts(data); + onSuccess(t('app.admin.store.products.successfully_deleted')); + } catch (e) { + onError(t('app.admin.store.products.unable_to_delete') + e); + } + }; + + return ( +
+

{t('app.admin.store.products.all_products')}

+ {t('app.admin.store.products.create_a_product')} + +
+ ); +}; + +const ProductsWrapper: React.FC = ({ onSuccess, onError }) => { + return ( + + + + ); +}; + +Application.Components.component('products', react2angular(ProductsWrapper, ['onSuccess', 'onError'])); diff --git a/app/frontend/src/javascript/controllers/admin/store.js b/app/frontend/src/javascript/controllers/admin/store.js index 76cac12da..f46365752 100644 --- a/app/frontend/src/javascript/controllers/admin/store.js +++ b/app/frontend/src/javascript/controllers/admin/store.js @@ -4,9 +4,35 @@ */ 'use strict'; -Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'growl', - function ($scope, CSRF, growl) { +Application.Controllers.controller('AdminStoreController', ['$scope', 'CSRF', 'growl', '$state', + function ($scope, CSRF, growl, $state) { + /* PRIVATE SCOPE */ + // Map of tab state and index + const TABS = { + 'app.admin.store.settings': 0, + 'app.admin.store.products': 1, + 'app.admin.store.categories': 2, + 'app.admin.store.orders': 3 + }; + /* PUBLIC SCOPE */ + // default tab: products + $scope.tabs = { + active: TABS[$state.current.name] + }; + + /** + * Callback triggered in click tab + */ + $scope.selectTab = () => { + setTimeout(function () { + const currentTab = _.keys(TABS)[$scope.tabs.active]; + if (currentTab !== $state.current.name) { + $state.go(currentTab, { location: true, notify: false, reload: false }); + } + }); + }; + /** * Callback triggered in case of error */ diff --git a/app/frontend/src/javascript/controllers/main_nav.js b/app/frontend/src/javascript/controllers/main_nav.js index c2a672e02..ead43fda0 100644 --- a/app/frontend/src/javascript/controllers/main_nav.js +++ b/app/frontend/src/javascript/controllers/main_nav.js @@ -84,7 +84,7 @@ Application.Controllers.controller('MainNavController', ['$scope', 'settingsProm authorizedRoles: ['admin', 'manager'] }, { - state: 'app.admin.store', + state: 'app.admin.store.products', linkText: 'app.public.common.manage_the_store', linkIcon: 'cart-plus', authorizedRoles: ['admin', 'manager'] diff --git a/app/frontend/src/javascript/models/product.ts b/app/frontend/src/javascript/models/product.ts new file mode 100644 index 000000000..b5818c1f4 --- /dev/null +++ b/app/frontend/src/javascript/models/product.ts @@ -0,0 +1,24 @@ +export enum StockType { + internal = 'internal', + external = 'external' +} + +export interface Stock { + internal: number, + external: number, +} + +export interface Product { + id: number, + name: string, + slug: string, + sku: string, + description: string, + is_active: boolean, + product_category_id: number, + amount: number, + quantity_min: number, + stock: Stock, + low_stock_alert: boolean, + low_stock_threshold: number, +} diff --git a/app/frontend/src/javascript/router.js b/app/frontend/src/javascript/router.js index d86e624a6..da074335a 100644 --- a/app/frontend/src/javascript/router.js +++ b/app/frontend/src/javascript/router.js @@ -1105,6 +1105,7 @@ angular.module('application.router', ['ui.router']) }) .state('app.admin.store', { + abstract: true, url: '/admin/store', views: { 'main@': { @@ -1114,6 +1115,22 @@ angular.module('application.router', ['ui.router']) } }) + .state('app.admin.store.settings', { + url: '/settings' + }) + + .state('app.admin.store.products', { + url: '/products' + }) + + .state('app.admin.store.categories', { + url: '/categories' + }) + + .state('app.admin.store.orders', { + url: '/orders' + }) + // OpenAPI Clients .state('app.admin.open_api_clients', { url: '/open_api_clients', diff --git a/app/frontend/templates/admin/store/index.html b/app/frontend/templates/admin/store/index.html index cf591b0ec..0b7557e96 100644 --- a/app/frontend/templates/admin/store/index.html +++ b/app/frontend/templates/admin/store/index.html @@ -20,19 +20,19 @@
- + - + - + - + diff --git a/app/frontend/templates/admin/store/products.html b/app/frontend/templates/admin/store/products.html index c4db68bf6..e37bcce4f 100644 --- a/app/frontend/templates/admin/store/products.html +++ b/app/frontend/templates/admin/store/products.html @@ -1 +1 @@ -

Products page

+ diff --git a/config/locales/app.admin.en.yml b/config/locales/app.admin.en.yml index d9c24536e..dede5aae3 100644 --- a/config/locales/app.admin.en.yml +++ b/config/locales/app.admin.en.yml @@ -1925,4 +1925,9 @@ en: success: "The category has been successfully deleted" save: "Save" required: "This field is required" - slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen" \ No newline at end of file + slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen" + products: + all_products: "All products" + create_a_product: "Create a product" + successfully_deleted: "The product has been successfully deleted" + unable_to_delete: "Unable to delete the product: " diff --git a/config/locales/app.admin.fr.yml b/config/locales/app.admin.fr.yml index 2bbcea278..991a429e4 100644 --- a/config/locales/app.admin.fr.yml +++ b/config/locales/app.admin.fr.yml @@ -1925,4 +1925,9 @@ fr: success: "La catégorie a bien été supprimée" save: "Enregistrer" required: "This field is required" - slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen" \ No newline at end of file + slug_pattern: "Only lowercase alphanumeric groups of characters separated by an hyphen" + products: + all_products: "Tous les produits" + create_a_product: "Créer un produit" + successfully_deleted: "Le produit a bien été supprimé" + unable_to_delete: "Impossible de supprimer le produit: "