1
0
mirror of https://github.com/LaCasemate/fab-manager.git synced 2025-01-18 07:52:23 +01:00

(quality) Create state label component

This commit is contained in:
vincent 2022-09-07 15:35:46 +02:00
parent 874be0bcb6
commit 23488284e8
26 changed files with 227 additions and 149 deletions

View File

@ -0,0 +1,18 @@
import React from 'react';
interface FabStateLabelProps {
status?: string,
background?: boolean
}
/**
* Render a label preceded by a bot
*/
export const FabStateLabel: React.FC<FabStateLabelProps> = ({ status, background, children }) => {
console.log('status: ', status);
return (
<span className={`fab-state-label ${status !== undefined ? status : ''} ${background ? 'bg' : ''}`}>
{children}
</span>
);
};

View File

@ -3,38 +3,71 @@ import { useTranslation } from 'react-i18next';
import { Order } from '../../models/order';
import FormatLib from '../../lib/format';
import { FabButton } from '../base/fab-button';
import { User } from '../../models/user';
import { FabStateLabel } from '../base/fab-state-label';
interface OrderItemProps {
order?: Order
statusColor: string
order?: Order,
currentUser?: User
}
/**
* List item for an order
*/
export const OrderItem: React.FC<OrderItemProps> = ({ order, statusColor }) => {
const { t } = useTranslation('admin');
export const OrderItem: React.FC<OrderItemProps> = ({ order, currentUser }) => {
const { t } = useTranslation('shared');
/**
* Go to order page
*/
const showOrder = (token: string) => {
window.location.href = `/#!/admin/store/o/${token}`;
const showOrder = (ref: string) => {
isPrivileged()
? window.location.href = `/#!/admin/store/o/${ref}`
: window.location.href = `/#!/store/o/${ref}`;
};
/**
* Check if the current operator has administrative rights or is a normal member
*/
const isPrivileged = (): boolean => {
return (currentUser?.role === 'admin' || currentUser?.role === 'manager');
};
/**
* Returns a className according to the status
*/
const statusColor = (status: string) => {
switch (status) {
case 'error':
return 'error';
case 'canceled':
return 'canceled';
case 'pending' || 'under_preparation':
return 'pending';
default:
return 'normal';
}
};
return (
<div className='order-item'>
<p className="ref">order.token</p>
<span className={`order-status ${statusColor}`}>order.state</span>
<div className='client'>
<span>{t('app.admin.store.order_item.client')}</span>
<p>order.user.name</p>
<p className="ref">order.ref</p>
<div>
<FabStateLabel status={statusColor('pending')} background>
order.state
</FabStateLabel>
</div>
{isPrivileged() &&
<div className='client'>
<span>{t('app.shared.store.order_item.client')}</span>
<p>order.user.name</p>
</div>
}
<p className="date">order.created_at</p>
<div className='price'>
<span>{t('app.admin.store.order_item.total')}</span>
<span>{t('app.shared.store.order_item.total')}</span>
<p>{FormatLib.price(order?.total)}</p>
</div>
<FabButton onClick={() => showOrder('orderToken')} icon={<i className="fas fa-eye" />} className="is-black" />
<FabButton onClick={() => showOrder('orderRef')} icon={<i className="fas fa-eye" />} className="is-black" />
</div>
);
};

View File

@ -4,6 +4,7 @@ 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';
declare const Application: IApplication;
@ -29,8 +30,8 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
*/
const buildOptions = (): Array<selectOption> => {
return [
{ value: 0, label: t('app.public.store.orders_dashboard.sort.newest') },
{ value: 1, label: t('app.public.store.orders_dashboard.sort.oldest') }
{ value: 0, label: t('app.public.orders_dashboard.sort.newest') },
{ value: 1, label: t('app.public.orders_dashboard.sort.oldest') }
];
};
/**
@ -43,7 +44,7 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
return (
<section className="orders-dashboard">
<header>
<h2>{t('app.public.store.orders_dashboard.heading')}</h2>
<h2>{t('app.public.orders_dashboard.heading')}</h2>
</header>
<div className="store-list">
@ -52,6 +53,9 @@ export const OrdersDashboard: React.FC<OrdersDashboardProps> = ({ onError }) =>
selectOptions={buildOptions()}
onSelectOptionsChange={handleSorting}
/>
<div className="orders-list">
<OrderItem />
</div>
</div>
</section>
);

View File

@ -9,10 +9,12 @@ import { StoreListHeader } from './store-list-header';
import { AccordionItem } from './accordion-item';
import { OrderItem } from './order-item';
import { MemberSelect } from '../user/member-select';
import { User } from '../../models/user';
declare const Application: IApplication;
interface OrdersProps {
currentUser?: User,
onSuccess: (message: string) => void,
onError: (message: string) => void,
}
@ -32,7 +34,8 @@ type checklistOption = { value: number, label: string };
*/
// TODO: delete next eslint disable
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Orders: React.FC<OrdersProps> = ({ onSuccess, onError }) => {
const Orders: React.FC<OrdersProps> = ({ currentUser, onSuccess, onError }) => {
console.log('currentUser: ', currentUser);
const { t } = useTranslation('admin');
const [filters, setFilters] = useImmer<Filters>(initFilters);
@ -123,22 +126,6 @@ const Orders: React.FC<OrdersProps> = ({ onSuccess, onError }) => {
setAccordion({ ...accordion, [id]: state });
};
/**
* Returns a className according to the status
*/
const statusColor = (status: string) => {
switch (status) {
case 'error':
return 'error';
case 'canceled':
return 'canceled';
case 'pending' || 'under_preparation':
return 'pending';
default:
return 'normal';
}
};
return (
<div className='orders'>
<header>
@ -209,10 +196,7 @@ const Orders: React.FC<OrdersProps> = ({ onSuccess, onError }) => {
onSelectOptionsChange={handleSorting}
/>
<div className="orders-list">
<OrderItem statusColor={statusColor('error')} />
<OrderItem statusColor={statusColor('canceled')} />
<OrderItem statusColor={statusColor('pending')} />
<OrderItem statusColor={statusColor('refunded')} />
<OrderItem currentUser={currentUser} />
</div>
</div>
</div>
@ -227,7 +211,7 @@ const OrdersWrapper: React.FC<OrdersProps> = (props) => {
);
};
Application.Components.component('orders', react2angular(OrdersWrapper, ['onSuccess', 'onError']));
Application.Components.component('orders', react2angular(OrdersWrapper, ['currentUser', 'onSuccess', 'onError']));
interface Filters {
reference: string,

View File

@ -5,6 +5,7 @@ import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product';
import { PencilSimple, Trash } from 'phosphor-react';
import noImage from '../../../../images/no_image.png';
import { FabStateLabel } from '../base/fab-state-label';
interface ProductItemProps {
product: Product,
@ -64,12 +65,12 @@ export const ProductItem: React.FC<ProductItemProps> = ({ product, onEdit, onDel
<p className="itemInfo-name">{product.name}</p>
</div>
<div className='details'>
<span className={`visibility ${product.is_active ? 'is-active' : ''}`}>
<FabStateLabel status={product.is_active ? 'is-active' : ''} background>
{product.is_active
? t('app.admin.store.product_item.visible')
: t('app.admin.store.product_item.hidden')
}
</span>
</FabStateLabel>
<div className={`stock ${product.stock.internal < product.low_stock_threshold ? 'low' : ''}`}>
<span>{t('app.admin.store.product_item.stock.internal')}</span>
<p>{product.stock.internal}</p>

View File

@ -12,6 +12,7 @@ import { FabButton } from '../base/fab-button';
import { PencilSimple } from 'phosphor-react';
import { FabModal, ModalSize } from '../base/fab-modal';
import { ProductStockModal } from './product-stock-modal';
import { FabStateLabel } from '../base/fab-state-label';
interface ProductStockFormProps<TFieldValues, TContext extends object> {
product: Product,
@ -135,7 +136,7 @@ export const ProductStockForm = <TFieldValues, TContext extends object> ({ produ
<HtmlTranslate trKey="app.admin.store.product_stock_form.stock_threshold_information" />
</FabAlert>
{activeThreshold && <>
<span className='stock-label'>{t('app.admin.store.product_stock_form.low_stock')}</span>
<FabStateLabel>{t('app.admin.store.product_stock_form.low_stock')}</FabStateLabel>
<div className="threshold-data-content">
<FormInput id="threshold"
type="number"

View File

@ -4,6 +4,7 @@ import { IApplication } from '../../models/application';
import { react2angular } from 'react2angular';
import { Loader } from '../base/loader';
import noImage from '../../../../images/no_image.png';
import { FabStateLabel } from '../base/fab-state-label';
declare const Application: IApplication;
@ -66,7 +67,9 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, onError, onSucce
<span>{t('app.admin.store.show_order.last_update')}</span>
<p>order.???</p>
</div>
<span className={`order-status ${statusColor('error')}`}>order.state</span>
<FabStateLabel status={statusColor('error')} background>
order.state
</FabStateLabel>
</div>
</div>
@ -99,7 +102,7 @@ export const ShowOrder: React.FC<ShowOrderProps> = ({ orderRef, onError, onSucce
</div>
</div>
<div className="group">
<div className="subgrid">
<div className="payment-info">
<label>{t('app.admin.store.show_order.payment_informations')}</label>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Ipsum rerum commodi quaerat possimus! Odit, harum.</p>

View File

@ -43,7 +43,7 @@ export const StoreListHeader: React.FC<StoreListHeaderProps> = ({ productsCount,
</div>
<div className="display">
<div className='sort'>
<p>{t('app.admin.store.store_list_header.display_options')}</p>
<p>{t('app.admin.store.store_list_header.sort')}</p>
<Select
options={selectOptions}
onChange={evt => onSelectOptionsChange(evt)}

View File

@ -4,6 +4,7 @@ import _ from 'lodash';
import { FabButton } from '../base/fab-button';
import { Product } from '../../models/product';
import { Order } from '../../models/order';
import { FabStateLabel } from '../base/fab-state-label';
import FormatLib from '../../lib/format';
import CartAPI from '../../api/cart';
import noImage from '../../../../images/no_image.png';
@ -57,6 +58,7 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
if (product.low_stock_alert) {
return 'low';
}
return '';
};
/**
@ -84,9 +86,9 @@ export const StoreProductItem: React.FC<StoreProductItemProps> = ({ product, car
<span>/ {t('app.public.store_product_item.unit')}</span>
</div>
}
<div className="stock-label">
<FabStateLabel status={statusColor(product)}>
{productStockStatus(product)}
</div>
</FabStateLabel>
{product.stock.external > 0 &&
<FabButton icon={<i className="fas fa-cart-arrow-down" />} className="main-action-btn" onClick={addProductToCart}>
{t('app.public.store_product_item.add')}

View File

@ -11,6 +11,7 @@ import ProductAPI from '../../api/product';
import noImage from '../../../../images/no_image.png';
import { FabButton } from '../base/fab-button';
import { FilePdf, Minus, Plus } from 'phosphor-react';
import { FabStateLabel } from '../base/fab-state-label';
declare const Application: IApplication;
@ -112,7 +113,7 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
if (product) {
return (
<div className={`store-product ${statusColor(product)}`}>
<div className={`store-product ${statusColor(product) || ''}`}>
<span className='ref'>ref: {product.sku}</span>
<h2 className='name'>{product.name}</h2>
<div className='gallery'>
@ -160,9 +161,9 @@ export const StoreProduct: React.FC<StoreProductProps> = ({ productSlug, onError
</div>
<aside>
<div className="stock-label">
<FabStateLabel status={statusColor(product)}>
{productStockStatus(product)}
</div>
</FabStateLabel>
<div className='price'>
<p>{FormatLib.price(product.amount)} <sup>TTC</sup></p>
<span>/ {t('app.public.store_product_item.unit')}</span>

View File

@ -4,8 +4,8 @@
*/
'use strict';
Application.Controllers.controller('ShowOrdersController', ['$scope', 'CSRF', 'growl', '$state', '$transition$',
function ($scope, CSRF, growl, $state, $transition$) {
Application.Controllers.controller('ShowOrdersController', ['$rootScope', '$scope', 'CSRF', 'growl', '$state', '$transition$',
function ($rootScope, $scope, CSRF, growl, $state, $transition$) {
/* PRIVATE SCOPE */
/* PUBLIC SCOPE */
@ -32,6 +32,9 @@ Application.Controllers.controller('ShowOrdersController', ['$scope', 'CSRF', 'g
$state.go('app.admin.store.orders');
};
// currently logged-in user
$scope.currentUser = $rootScope.currentUser;
/* PRIVATE SCOPE */
/**

View File

@ -631,6 +631,17 @@ angular.module('application.router', ['ui.router'])
}
})
// show order
.state('app.public.order_show', {
url: '/store/o/:token',
views: {
'main@': {
templateUrl: '/orders/show.html',
controller: 'ShowOrdersController'
}
}
})
// cart
.state('app.public.cart', {
url: '/cart',

View File

@ -27,6 +27,7 @@
@import "modules/base/fab-output-copy";
@import "modules/base/fab-panel";
@import "modules/base/fab-popover";
@import "modules/base/fab-state-label";
@import "modules/base/fab-text-editor";
@import "modules/base/labelled-input";
@import "modules/calendar/calendar";
@ -92,6 +93,7 @@
@import "modules/settings/user-validation-setting";
@import "modules/socials/fab-socials";
@import "modules/store/_utilities";
@import "modules/store/order-item";
@import "modules/store/orders-dashboard";
@import "modules/store/orders";
@import "modules/store/product-categories";

View File

@ -0,0 +1,28 @@
.fab-state-label {
// --status-color needs to be defined in the component's CSS
--status-color: var(--gray-hard-darkest);
display: flex;
align-items: center;
@include text-sm;
line-height: 1.714;
color: var(--status-color);
&.bg {
width: fit-content;
padding: 0.4rem 0.8rem;
justify-content: center;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
color: var(--gray-hard-darkest);
}
&::before {
flex-shrink: 0;
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
}
}

View File

@ -68,21 +68,6 @@
}
}
.stock-label {
display: flex;
align-items: center;
@include text-sm;
color: var(--status-color);
&::before {
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
}
}
// Custom scrollbar
.u-scrollbar {
&::-webkit-scrollbar-track

View File

@ -0,0 +1,47 @@
.order-item {
width: 100%;
display: flex;
gap: 2.4rem;
justify-items: flex-start;
align-items: center;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
& > *:not(button) { flex: 0 1 40%; }
p { margin: 0; }
.ref {
flex: 1 1 100%;
@include text-base(600);
}
.fab-state-label {
--status-color: var(--success);
&.error { --status-color: var(--alert); }
&.canceled { --status-color: var(--alert-light); }
&.pending { --status-color: var(--information); }
&.normal { --status-color: var(--success); }
margin: 0 auto;
}
.status .state-label { margin: 0 auto; }
.client {
display: flex;
flex-direction: column;
span {
@include text-xs;
color: var(--gray-hard-light);
}
p { @include text-sm; }
}
.date { @include text-sm; }
.price {
display: flex;
flex-direction: column;
justify-self: flex-end;
span {
@include text-xs;
color: var(--gray-hard-light);
}
p { @include text-base(600); }
}
}

View File

@ -11,4 +11,7 @@
padding-bottom: 0;
grid-column: 2 / -2;
}
.store-list {
grid-column: 2 / -2;
}
}

View File

@ -17,39 +17,6 @@
& > *:not(:first-child) {
margin-top: 1.6rem;
}
.order-item {
width: 100%;
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr 15rem 15rem 10ch 12rem;
gap: 2.4rem;
justify-items: flex-start;
align-items: center;
padding: 1.6rem;
border: 1px solid var(--gray-soft-dark);
border-radius: var(--border-radius);
background-color: var(--gray-soft-lightest);
p { margin: 0; }
.ref { @include text-base(600); }
.client {
span {
@include text-xs;
color: var(--gray-hard-light);
}
p { @include text-sm; }
}
.date { @include text-sm; }
.price {
justify-self: flex-end;
span {
@include text-xs;
color: var(--gray-hard-light);
}
p { @include text-base(600); }
}
}
}
}
@ -86,6 +53,8 @@
line-height: 1.18;
}
.group {
display: flex;
flex-direction: column;
span {
@include text-xs;
color: var(--gray-hard-light);
@ -93,7 +62,7 @@
}
}
& > .group {
.subgrid {
grid-column: 2 / -2;
display: grid;
grid-template-columns: repeat(2, 1fr);
@ -130,29 +99,12 @@
}
}
}
}
.order-status {
--status-color: var(--success);
&.error { --status-color: var(--alert); }
&.canceled { --status-color: var(--alert-light); }
&.pending { --status-color: var(--information); }
&.normal { --status-color: var(--success); }
padding: 0.4rem 0.8rem;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
@include text-sm(500);
line-height: 1.714;
&::before {
content: "";
margin-right: 0.8rem;
width: 1rem;
height: 1rem;
background-color: var(--status-color);
border-radius: 50%;
.fab-state-label {
--status-color: var(--success);
&.error { --status-color: var(--alert); }
&.canceled { --status-color: var(--alert-light); }
&.pending { --status-color: var(--information); }
&.normal { --status-color: var(--success); }
}
}

View File

@ -37,7 +37,7 @@
label { flex: 0 1 fit-content; }
}
.stock-label {
.fab-state-label {
--status-color: var(--alert-light);
}

View File

@ -49,7 +49,12 @@
word-break: break-all;
}
}
.stock-label { grid-area: stock; }
.fab-state-label {
--status-color: var(--success);
&.low { --status-color: var(--alert-light); }
&.out-of-stock { --status-color: var(--alert); }
grid-area: stock;
}
button {
grid-area: btn;
align-self: flex-end;

View File

@ -54,25 +54,8 @@
@include text-base(600);
}
.visibility {
justify-self: center;
padding: 0.4rem 0.8rem;
display: flex;
align-items: center;
background-color: var(--gray-soft-light);
border-radius: var(--border-radius);
&::before {
flex-shrink: 0;
margin-right: 1rem;
content: "";
width: 1rem;
height: 1rem;
background-color: var(--gray-hard);
border-radius: 50%;
}
&.is-active::before {
background-color: var(--success);
}
.fab-state-label.is-active {
--status-color: var(--success);
}
.stock {
display: flex;

View File

@ -142,6 +142,12 @@
background-color: var(--gray-soft-light);
border-radius: var(--border-radius-sm);
.fab-state-label {
--status-color: var(--success);
&.low { --status-color: var(--alert-light); }
&.out-of-stock { --status-color: var(--alert); }
}
.price {
p {
margin: 0;

View File

@ -1 +1 @@
<orders on-success="onSuccess" on-error="onError"/>
<orders current-user="currentUser" on-success="onSuccess" on-error="onError"/>

View File

@ -1949,7 +1949,7 @@ en:
price_high: "Price: high to low"
store_list_header:
result_count: "Result count:"
display_options: "Display options:"
sort: "Sort:"
visible_only: "Visible products only"
product_item:
visible: "visible"
@ -2049,9 +2049,6 @@ en:
sort:
newest: "Newest first"
oldest: "Oldest first"
order_item:
total: "Total"
client: "Client"
show_order:
see_invoice: "See invoice"
client: "Client"

View File

@ -392,6 +392,11 @@ fr:
store_cart:
checkout: "Valider mon panier"
cart_is_empty: "Votre panier est vide"
orders_dashboard:
heading: "My orders"
sort:
newest: "Newest first"
oldest: "Oldest first"
member_select:
select_a_member: "Sélectionnez un membre"
start_typing: "Commencez à écrire..."

View File

@ -560,3 +560,7 @@ en:
browse: "Browse"
edit: "Edit"
main_image: "Main image"
store:
order_item:
total: "Total"
client: "Client"