1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-02-20 14:54:15 +01:00

Merge branch 'product-store' of git.sleede.com:projets/fab-manager into product-store

This commit is contained in:
Du Peng 2022-10-04 16:52:04 +02:00
commit a9dc7b4dd7
12 changed files with 83 additions and 48 deletions

View File

@ -1,14 +1,14 @@
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 { StoreListHeader } from './store-list-header';
import { OrderItem } from './order-item';
import { FabPagination } from '../base/fab-pagination';
import OrderAPI from '../../api/order';
import { Order } from '../../models/order';
import { User } from '../../models/user';
import { Loader } from '../../base/loader';
import { IApplication } from '../../../models/application';
import { StoreListHeader } from '../../store/store-list-header';
import { OrderItem } from '../../store/order-item';
import { FabPagination } from '../../base/fab-pagination';
import OrderAPI from '../../../api/order';
import { Order, OrderSortOption } from '../../../models/order';
import { User } from '../../../models/user';
declare const Application: IApplication;
@ -20,7 +20,7 @@ interface OrdersDashboardProps {
* Option format, expected by react-select
* @see https://github.com/JedWatson/react-select
*/
type selectOption = { value: number, label: string };
type selectOption = { value: OrderSortOption, label: string };
/**
* This component shows a list of all orders from the store for the current user
@ -46,15 +46,15 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ currentUser, o
*/
const buildOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.public.orders_dashboard.sort.newest') },
{ value: 1, label: t('app.public.orders_dashboard.sort.oldest') }
{ value: 'created_at-desc', label: t('app.public.orders_dashboard.sort.newest') },
{ value: 'created_at-asc', label: t('app.public.orders_dashboard.sort.oldest') }
];
};
/**
* Display option: sorting
*/
const handleSorting = (option: selectOption) => {
OrderAPI.index({ page: 1, sort: option.value ? 'ASC' : 'DESC' }).then(res => {
OrderAPI.index({ page: 1, sort: option.value }).then(res => {
setCurrentPage(1);
setOrders(res.data);
setPageCount(res.total_pages);

View File

@ -73,7 +73,7 @@ export const ManageProductCategory: React.FC<ManageProductCategoryProps> = ({ pr
isOpen={isOpen}
toggleModal={toggleModal}
closeButton>
{ (action === 'update' || action === 'delete') && <p className='subtitle'>{productCategory.name}</p>}
{ action === 'update' && <p className='subtitle'>{productCategory.name}</p>}
<ProductCategoryForm action={action}
productCategories={productCategories}
productCategory={productCategory}

View File

@ -6,8 +6,8 @@ import { FormInput } from '../../form/form-input';
import { FormSelect } from '../../form/form-select';
import { ProductCategory } from '../../../models/product-category';
import { FabButton } from '../../base/fab-button';
import { FabAlert } from '../../base/fab-alert';
import ProductCategoryAPI from '../../../api/product-category';
import { HtmlTranslate } from '../../base/html-translate';
interface ProductCategoryFormProps {
action: 'create' | 'update' | 'delete',
@ -95,10 +95,8 @@ export const ProductCategoryForm: React.FC<ProductCategoryFormProps> = ({ action
<form onSubmit={handleSubmit(onSubmit)} name="productCategoryForm" className="product-category-form">
{ action === 'delete'
? <>
<FabAlert level='danger'>
{t('app.admin.store.product_category_form.delete.confirm')}
</FabAlert>
<FabButton type='submit'>{t('app.admin.store.product_category_form.save')}</FabButton>
<HtmlTranslate trKey="app.admin.store.product_category_form.delete.confirm" options={{ CATEGORY: productCategory?.name }} />
<FabButton type='submit'>{t('app.admin.store.product_category_form.delete.save')}</FabButton>
</>
: <>
<FormInput id='name'

View File

@ -48,14 +48,16 @@ export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
}
<div className="date">
<span>{t('app.shared.store.order_item.created_at')}</span>
<div>{FormatLib.date(order.created_at)}
<div className="fab-tooltip">
<span className="trigger"><PlusCircle size={16} weight="light" /></span>
<div className="content">
{t('app.shared.store.order_item.last_update')}<br />
{FormatLib.date(order.updated_at)}
<div>
<p>{FormatLib.date(order.created_at)}
<div className="fab-tooltip">
<span className="trigger"><PlusCircle size={16} weight="light" /></span>
<div className="content">
{t('app.shared.store.order_item.last_update')}<br />
{FormatLib.date(order.updated_at)}
</div>
</div>
</div>
</p>
</div>
</div>
<div className='price'>

View File

@ -11,6 +11,8 @@ import { Order } from '../../models/order';
import FormatLib from '../../lib/format';
import OrderLib from '../../lib/order';
import { OrderActions } from './order-actions';
import SettingAPI from '../../api/setting';
import { SettingName } from '../../models/setting';
declare const Application: IApplication;
@ -28,11 +30,15 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onSu
const { t } = useTranslation('shared');
const [order, setOrder] = useState<Order>();
const [settings, setSettings] = useState<Map<SettingName, string>>(null);
useEffect(() => {
OrderAPI.get(orderId).then(data => {
setOrder(data);
}).catch(onError);
SettingAPI.query(['store_withdrawal_instructions', 'fablab_name'])
.then(res => setSettings(res))
.catch(onError);
}, []);
/**
@ -72,6 +78,17 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onSu
return paymentVerbose;
};
/**
* Text instructions for the customer
*/
const withdrawalInstructions = (): string => {
const instructions = settings?.get('store_withdrawal_instructions');
if (instructions) {
return instructions;
}
return t('app.shared.store.show_order.please_contact_FABLAB', { FABLAB: settings?.get('fablab_name') });
};
/**
* Callback after action success
*/
@ -172,6 +189,10 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderId, currentUser, onSu
}
<p className='total'>{t('app.shared.store.show_order.cart_total')} <span>{FormatLib.price(OrderLib.paidTotal(order))}</span></p>
</div>
<div className="withdrawal-instructions">
<label>{t('app.shared.store.show_order.pickup')}</label>
<p dangerouslySetInnerHTML={{ __html: withdrawalInstructions() }} />
</div>
</div>
</div>
);

View File

@ -28,7 +28,6 @@ import { ActiveFiltersTags } from './filters/active-filters-tags';
import ProductLib from '../../lib/product';
import { UIRouter } from '@uirouter/angularjs';
import SettingAPI from '../../api/setting';
import { CaretDoubleDown } from 'phosphor-react';
declare const Application: IApplication;
@ -70,13 +69,14 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
const [resources, setResources] = useImmer<ProductResourcesFetching>(storeInitialResources);
const [machinesModule, setMachinesModule] = useState<boolean>(false);
const [categoriesTree, setCategoriesTree] = useState<CategoryTree[]>([]);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [filtersPanel, setFiltersPanel] = useState<boolean>(false);
const [pageCount, setPageCount] = useState<number>(0);
const [productsCount, setProductsCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1);
useEffect(() => {
fetchProducts().then(scrollToProducts);
fetchProducts();
ProductLib.fetchInitialResources(setResources, onError, formatCategories);
SettingAPI.get('machines_module').then(data => {
setMachinesModule(data.value === 'true');
@ -85,7 +85,7 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
useEffect(() => {
if (resources.filters.ready) {
fetchProducts().then(scrollToProducts);
fetchProducts();
uiRouter.stateService.transitionTo(uiRouter.globals.current, ProductLib.indexFiltersToRouterParams(resources.filters.data));
}
}, [resources.filters]);
@ -221,13 +221,6 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
}
};
/**
* Scroll the view to the product list
*/
const scrollToProducts = () => {
window.document.getElementById('content-main').scrollTo({ top: 100, behavior: 'smooth' });
};
const selectedCategory = resources.filters.data.categories[0];
const parent = resources.categories.data.find(c => c.id === selectedCategory?.parent_id);
return (
@ -252,13 +245,6 @@ const Store: React.FC<StoreProps> = ({ onError, onSuccess, currentUser, uiRouter
}
</ul>
<aside className={`store-filters ${filtersPanel ? '' : 'collapsed'}`}>
<header>
<h3>{t('app.public.store.products.filter')}</h3>
<div className='grpBtn'>
<FabButton onClick={clearAllFilters} className="is-black">{t('app.public.store.products.filter_clear')}</FabButton>
<CaretDoubleDown className='filters-toggle' size={16} weight="bold" onClick={() => setFiltersPanel(!filtersPanel)} />
</div>
</header>
<div className='grp'>
<div className="categories">
<header>

View File

@ -99,6 +99,7 @@
@import "modules/store/orders-dashboard";
@import "modules/store/orders";
@import "modules/store/product-categories";
@import "modules/store/product-category-form";
@import "modules/store/product-form";
@import "modules/store/product-stock-form";
@import "modules/store/product-stock-modal";

View File

@ -1,8 +1,8 @@
.order-item {
width: 100%;
display: grid;
grid-template-rows: repeat(2, min-content);
grid-template-columns: 2fr 1fr 10ch;
grid-template-rows: repeat(3, min-content);
grid-template-columns: 1fr 1fr;
gap: 1.6rem 2.4rem;
align-items: center;
padding: 1.6rem;
@ -52,7 +52,7 @@
}
}
.price {
grid-area: 1 / 3 / 3 / 4;
grid-area: 3 / 1 / 4 / 2;
display: flex;
flex-direction: column;
justify-self: flex-end;
@ -62,7 +62,25 @@
}
p { @include text-base(600); }
}
button { grid-area: 1 / 4 / 3 / 5; }
button {
grid-area: 3 / 2 / 4 / 3;
justify-self: flex-end;
width: 4rem;
padding: 0;
display: flex;
justify-content: center;
}
@media (min-width: 640px) {
grid-template-rows: repeat(2, min-content);
grid-template-columns: 2fr 1fr 10ch;
.ref { grid-area: 1 / 1 / 2 / 2; }
.fab-state-label { grid-area: 1 / 2 / 2 / 3; }
.client { grid-area: 2 / 1 / 3 / 2; }
.date { grid-area: 2 / 2 / 3 / 3; }
.price { grid-area: 1 / 3 / 3 / 4; }
button { grid-area: 1 / 4 / 3 / 5; }
}
@media (min-width: 1440px) {
grid-auto-flow: column;

View File

@ -101,7 +101,8 @@
align-items: flex-start;
.payment-info,
.amount {
.amount,
.withdrawal-instructions {
padding: 2.4rem;
border: 1px solid var(--gray-soft);
border-radius: var(--border-radius);

View File

@ -0,0 +1,5 @@
.product-category-form {
span {
margin-bottom: 2.4rem;
}
}

View File

@ -1937,7 +1937,8 @@ en:
error: "Unable to modify the category: "
success: "The category has been modified."
delete:
confirm: "Do you really want to delete this product category?"
confirm: "Do you really want to delete <strong>{CATEGORY}</strong>?<br>If it has sub-categories, they will also be deleted."
save: "Delete"
error: "Unable to delete the category: "
success: "The category has been successfully deleted"
save: "Save"

View File

@ -592,6 +592,8 @@ en:
gift_total: "Discount total"
coupon: "Coupon"
cart_total: "Cart total"
pickup: "Pickup your products"
please_contact_FABLAB: "Please contact {FABLAB, select, undefined{us} other{{FABLAB}}} for withdrawal instructions."
state:
cart: 'Cart'
in_progress: 'Under preparation'