diff --git a/core/fsm/README.md b/core/fsm/README.md index 2bfb14714..ed034c9eb 100644 --- a/core/fsm/README.md +++ b/core/fsm/README.md @@ -1,3 +1,3 @@ # Alwatr Finite State Machines - `@alwatr/fsm` -Managing invocations finite-state machines as actors written in tiny TypeScript module. +Managing invocations finite-state machines for lit-element written in tiny TypeScript module. diff --git a/core/fsm/package.json b/core/fsm/package.json index 855a36f36..9cfe0f620 100644 --- a/core/fsm/package.json +++ b/core/fsm/package.json @@ -1,18 +1,21 @@ { "name": "@alwatr/fsm", "version": "0.30.0", - "description": "Managing invocations finite-state machines as actors written in tiny TypeScript module.", + "description": "Managing invocations finite-state machines for lit-element written in tiny TypeScript module.", "keywords": [ "state", "finite", "machine", + "lit", + "lit-element", + "lit-html", "typescript", "esm", "alwatr" ], - "main": "core.js", + "main": "index.js", "type": "module", - "types": "core.d.ts", + "types": "index.d.ts", "author": "S. Ali Mihandoost ", "license": "MIT", "files": [ diff --git a/core/fsm/src/core.ts b/core/fsm/src/core.ts index 62546737e..5c0e0517e 100644 --- a/core/fsm/src/core.ts +++ b/core/fsm/src/core.ts @@ -1,122 +1,373 @@ import {createLogger, globalAlwatr} from '@alwatr/logger'; -import {contextConsumer} from '@alwatr/signal'; -import {dispatch} from '@alwatr/signal/core.js'; +import {ListenerSpec, contextProvider, contextConsumer} from '@alwatr/signal'; +import {SubscribeOptions} from '@alwatr/signal/type.js'; -import type {Stringifyable, StringifyableRecord} from '@alwatr/type'; +import type { + ActionRecord, + FsmConstructor, + FsmConstructorConfig, + FsmConsumerInterface, + FsmInstance, + FsmState, + FsmTypeHelper, + SignalConfig, +} from './type.js'; +import type {OmitFirstParam, SingleOrArray, StringifyableRecord} from '@alwatr/type'; globalAlwatr.registeredList.push({ name: '@alwatr/fsm', version: _ALWATR_VERSION_, }); -export interface MachineConfig - extends StringifyableRecord { - /** - * Machine ID (It is used in the state change signal identifier, so it must be unique). - */ - id: string; - - /** - * Initial state. - */ - initial: TState; - - /** - * Initial context. - */ - context: TContext; - - /** - * States list - */ - states: { - [S in TState | '$all']: { - /** - * An object mapping eventId (keys) to state. - */ - on: { - [E in TEventId]?: TState | '$self'; - }; - }; - }; -} +const logger = createLogger(`alwatr/fsm`); -export interface StateContext { - [T: string]: string; - to: TState; - from: TState | 'init'; - by: TEventId | 'INIT'; -} +/** + * Finite state machine constructor storage. + */ +const fsmConstructorStorage: Record = {}; -export class FiniteStateMachine< +export const defineConstructor = < TState extends string = string, TEventId extends string = string, + TActionName extends string = string, TContext extends StringifyableRecord = StringifyableRecord -> { - state: StateContext = { - to: this.config.initial, - from: 'init', - by: 'INIT', +>( + id: string, + config: FsmConstructorConfig, + ): FsmConstructorConfig => { + logger.logMethodArgs('defineConstructor', {id, config}); + if (fsmConstructorStorage[id] != null) throw new Error('fsm_exist', {cause: {id}}); + fsmConstructorStorage[id] = { + id, + config, + actionRecord: {}, + signalList: [], }; - context = this.config.context; - signal = contextConsumer.bind>('finite-state-machine-' + this.config.id); + return config; +}; + +export const getFsmInstance = < + TState extends string = string, + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>( + instanceId: string, + ): FsmInstance => { + logger.logMethodArgs('_getFsmInstance', instanceId); + const fsmInstance = contextConsumer.getValue>(instanceId); + if (fsmInstance == null) throw new Error('fsm_undefined', {cause: {instanceId}}); + return fsmInstance; +}; + +export const getFsmConstructor = (constructorId: string): FsmConstructor => { + logger.logMethodArgs('_getFsmConstructor', constructorId); + const fsmConstructor = fsmConstructorStorage[constructorId]; + if (fsmConstructor == null) throw new Error('fsm_undefined', {cause: {constructorId: constructorId}}); + return fsmConstructor; +}; + +export const getState = ( + instanceId: string, +): FsmState => { + logger.logMethodArgs('getState', instanceId); + return getFsmInstance(instanceId).state; +}; - protected _logger = createLogger(`alwatr/fsm:${this.config.id}`); +export const getContext = ( + instanceId: string, +): TContext => { + logger.logMethodArgs('getContext', instanceId); + return getFsmInstance(instanceId).context; +}; + +export const setContext = ( + instanceId: string, + context: Partial, + notify?: boolean, +): void => { + logger.logMethodArgs('setContext', {instanceId, context}); + const fsmInstance = getFsmInstance(instanceId); + fsmInstance.context = { + ...fsmInstance.context, + ...context, + }; + + if (notify) { + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'Timeout'}); + } +}; + +export const transition = < + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +>( + instanceId: string, + event: TEventId, + context?: Partial, + ): void => { + const fsmInstance = getFsmInstance(instanceId); + const fsmConstructor = getFsmConstructor(fsmInstance.constructorId); + const fromState = fsmInstance.state.target; + const stateRecord = fsmConstructor.config.stateRecord; + const transitionConfig = stateRecord[fromState]?.on[event] ?? stateRecord.$all.on[event]; - protected setState(to: TState, by: TEventId | 'INIT'): void { - this.state = { - to, - from: this.signal.getValue()?.to ?? 'init', - by, + logger.logMethodArgs('transition', {instanceId, fromState, event, context, target: transitionConfig?.target}); + + if (context !== undefined) { + fsmInstance.context = { + ...fsmInstance.context, + ...context, }; - dispatch>(this.signal.id, this.state, {debounce: 'NextCycle'}); } - constructor(public readonly config: Readonly>) { - this._logger.logMethodArgs('constructor', config); - dispatch>(this.signal.id, this.state, {debounce: 'NextCycle'}); - if (!config.states[config.initial]) { - this._logger.error('constructor', 'invalid_initial_state', config); - } + if (transitionConfig == null) { + logger.incident( + 'transition', + 'invalid_target_state', + 'Defined target state for this event not found in state config', + { + fromState, + event, + events: { + ...stateRecord.$all?.on, + ...stateRecord[fromState]?.on, + }, + }, + ); + return; } - /** - * Machine transition. - */ - transition(event: TEventId, context?: Partial): TState | null { - const fromState = this.state.to; + const consumerInterface = finiteStateMachineConsumer(instanceId); - let toState: TState | '$self' | undefined = - this.config.states[fromState]?.on?.[event] ?? this.config.states.$all?.on?.[event]; + if (transitionConfig.condition) { + if (_execAction(fsmConstructor, transitionConfig.condition, consumerInterface) === false) return; + } - if (toState === '$self') { - toState = fromState; - } + fsmInstance.state = { + target: transitionConfig.target ?? fromState, + from: fromState, + by: event, + }; - this._logger.logMethodFull('transition', {fromState, event, context}, toState); + contextProvider.setValue(instanceId, fsmInstance, {debounce: 'Timeout'}); - if (context !== undefined) { - this.context = { - ...this.context, - ...context, - }; - } + _execAllActions(fsmConstructor, fsmInstance.state, consumerInterface); +}; - if (toState == null) { - this._logger.incident( - 'transition', - 'invalid_target_state', - 'Defined target state for this event not found in state config', - { - fromState, - event, - events: {...this.config.states.$all?.on, ...this.config.states[fromState]?.on}, - }, - ); - return null; +export const defineActions = (constructorId: string, actionRecord: ActionRecord): void => { + logger.logMethodArgs('defineActions', {constructorId, actionRecord}); + const fmsConstructor = getFsmConstructor(constructorId); + fmsConstructor.actionRecord = { + ...fmsConstructor.actionRecord, + ...actionRecord, + }; +}; + +export const _execAllActions = ( + constructor: FsmConstructor, + state: FsmState, + consumerInterface: FsmConsumerInterface, +): void => { + logger.logMethodArgs('_execAllActions', consumerInterface.id); + + const stateRecord = constructor.config.stateRecord; + + if (state.by === 'INIT') { + _execAction(constructor, stateRecord.$all.entry, consumerInterface); + _execAction(constructor, stateRecord[state.target]?.entry, consumerInterface); + return; + } + + // else + if (state.from !== state.target) { + _execAction(constructor, stateRecord.$all.exit, consumerInterface); + _execAction(constructor, stateRecord[state.from]?.exit, consumerInterface); + _execAction(constructor, stateRecord.$all.entry, consumerInterface); + _execAction(constructor, stateRecord[state.target]?.entry, consumerInterface); + } + + _execAction( + constructor, + stateRecord[state.from]?.on[state.by] != null + ? stateRecord[state.from].on[state.by]?.actions + : stateRecord.$all.on[state.by]?.actions, + consumerInterface, + ); +}; + +export const _execAction = ( + constructor: FsmConstructor, + actionNames: SingleOrArray | undefined, + finiteStateMachine: FsmConsumerInterface, +): boolean | void => { + if (actionNames == null) return; + logger.logMethodArgs('execAction', actionNames); + + if (Array.isArray(actionNames)) { + return actionNames + .map((actionName) => _execAction(constructor, actionName, finiteStateMachine)) + .every((r) => r === true); + } + + try { + const actionFn = constructor.actionRecord[actionNames]; + if (actionFn == null) { + return logger.error('execAction', 'action_not_found', { + actionNames, + constructorId: constructor.id, + instanceId: finiteStateMachine.id, + }); } + return actionFn(finiteStateMachine); + } + catch (error) { + return logger.error('execAction', 'action_error', error, { + actionNames, + constructorId: constructor.id, + instanceId: finiteStateMachine.id, + }); + } +}; + +export const initFsmInstance = (instanceId: string, constructorId: string): void => { + logger.logMethodArgs('initializeMachine', {constructorId, instanceId}); + const {initial, context} = getFsmConstructor(constructorId).config; + contextProvider.setValue( + instanceId, + { + constructorId, + state: { + target: initial, + from: initial, + by: 'INIT', + }, + context, + signalList: [], + }, + {debounce: 'NextCycle'}, + ); +}; + +export const subscribeSignals = ( + instanceId: string, + signalList: Array, + subscribeConstructorSignals = true, +): Array => { + logger.logMethodArgs('subscribeSignals', {instanceId, signalList}); + const listenerList: Array = []; + + if (subscribeConstructorSignals) { + signalList = signalList.concat(getFsmConstructor(getFsmInstance(instanceId).constructorId).signalList); + } + + for (const signalConfig of signalList) { + listenerList.push( + contextConsumer.subscribe( + signalConfig.signalId ?? instanceId, + (signalDetail: StringifyableRecord): void => { + if (signalConfig.callback) { + signalConfig.callback(signalDetail, finiteStateMachineConsumer(instanceId)); + } + else { + // prettier-ignore + transition( + instanceId, + signalConfig.transition, + signalConfig.contextName + ? { + [signalConfig.contextName]: signalDetail, + } + : undefined, + ); + } + }, + {receivePrevious: signalConfig.receivePrevious ?? 'No'}, + ), + ); + } - this.setState(toState, event); - return toState; + return listenerList; +}; + +// protected unsubscribeSignals(): void { +// if (this._listenerList.length === 0) return; +// for (const listener of this._listenerList) { +// eventListener.unsubscribe(listener); +// } +// this._listenerList.length = 0; +// } + +export const defineConstructorSignals = ( + constructorId: string, + signalList: Array>, +): void => { + logger.logMethodArgs('defineSignals', {constructorId, signalList: signalList}); + const fsmConstructor = getFsmConstructor(constructorId); + fsmConstructor.signalList = fsmConstructor.signalList.concat(signalList as Array); +}; + +export const defineInstanceSignals = ( + instanceId: string, + signalList: Array>, + subscribeConstructorSignals = true, +): Array => { + logger.logMethodArgs('defineSignals', {instanceId, signals: signalList}); + return subscribeSignals(instanceId, signalList as Array, subscribeConstructorSignals); +}; + +export const render = ( + instanceId: string, + states: {[P in TState]: (() => unknown) | TState}, +): unknown => { + const state = getFsmInstance(instanceId).state; + logger.logMethodArgs('render', {instanceId, state: state.target}); + let renderFn = states[state.target as TState]; + + if (typeof renderFn === 'string') { + renderFn = states[renderFn as TState]; + } + + if (typeof renderFn === 'function') { + return renderFn(); } -} + + return; +}; + +export const subscribe = ( + instanceId: string, + callback: () => void, + options?: Partial, +): ListenerSpec => { + logger.logMethodArgs('subscribe', instanceId); + return contextConsumer.subscribe(instanceId, callback, options); +}; + +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type +export const finiteStateMachineConsumer = ( + instanceId: string, + makeFromConstructor?: string, +) => { + logger.logMethodArgs('stateMachineLookup', instanceId); + + const machineInstance = contextConsumer.getValue(instanceId); + if (machineInstance == null) { + // instance not initialized. + if (makeFromConstructor == null) { + throw new Error('fsm_undefined', {cause: {instanceId}}); + } + initFsmInstance(instanceId, makeFromConstructor); + } + + return { + id: instanceId, + constructorId: machineInstance?.constructorId ?? makeFromConstructor, + render: render.bind(null, instanceId) as OmitFirstParam>, + subscribe: subscribe.bind(null, instanceId) as OmitFirstParam, + getState: getState.bind(null, instanceId) as OmitFirstParam>, + getContext: getContext.bind(null, instanceId) as OmitFirstParam>, + setContext: setContext.bind(null, instanceId) as OmitFirstParam>, + transition: transition.bind(null, instanceId) as OmitFirstParam>, + defineSignals: defineInstanceSignals.bind(null, instanceId) as OmitFirstParam>, + } as const; +}; diff --git a/core/fsm/src/index.ts b/core/fsm/src/index.ts new file mode 100644 index 000000000..e03c9fcb1 --- /dev/null +++ b/core/fsm/src/index.ts @@ -0,0 +1,11 @@ +import {defineActions, defineConstructor, defineConstructorSignals, subscribe} from './core.js'; + +export {finiteStateMachineConsumer} from './core.js'; +export const finiteStateMachineProvider = { + defineConstructor, + defineActions, + defineSignals: defineConstructorSignals, + subscribe, +} as const; + +export type {FsmTypeHelper, FsmConstructorConfig} from './type.js'; diff --git a/core/fsm/src/type.ts b/core/fsm/src/type.ts new file mode 100644 index 000000000..d6b7b17e4 --- /dev/null +++ b/core/fsm/src/type.ts @@ -0,0 +1,143 @@ +import type {finiteStateMachineConsumer} from './core.js'; +import type {DebounceType} from '@alwatr/signal'; +import type {ArrayItems, SingleOrArray, StringifyableRecord} from '@alwatr/type'; + +export interface FsmConstructor { + /** + * Constructor id. + */ + readonly id: string; + readonly config: FsmConstructorConfig; + actionRecord: ActionRecord; + signalList: Array; +} + +export interface FsmConstructorConfig< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { + /** + * Initial context. + */ + readonly context: TContext; + + /** + * Initial state. + */ + readonly initial: TState; + + /** + * Define state list + */ + readonly stateRecord: StateRecord; +} + +export type StateRecord< + TState extends string = string, + TEventId extends string = string, + TActionName extends string = string +> = { + readonly [S in TState | '$all']: { + /** + * On state exit actions + */ + readonly exit?: SingleOrArray; + + /** + * On state entry actions + */ + readonly entry?: SingleOrArray; + + /** + * An object mapping eventId to state. + * + * Example: + * + * ```ts + * stateRecord: { + * on: { + * TIMER: { + * target: 'green', + * condition: () => car.gas > 0, + * actions: () => car.go(), + * } + * } + * } + * ``` + */ + readonly on: { + readonly [E in TEventId]?: TransitionConfig | undefined; + }; + }; +}; + +export interface FsmState + extends StringifyableRecord { + /** + * Current state + */ + target: TState; + /** + * Last state + */ + from: TState; + /** + * Transition event + */ + by: TEventId | 'INIT'; +} + +export interface TransitionConfig + extends StringifyableRecord { + readonly target?: TState; + readonly condition?: TActionName; + readonly actions?: SingleOrArray; +} + +export interface FsmInstance< + TState extends string = string, + TEventId extends string = string, + TContext extends StringifyableRecord = StringifyableRecord +> extends StringifyableRecord { + readonly constructorId: string; + state: FsmState; + context: TContext; +} + +export type ActionRecord = { + readonly [P in T['TActionName']]?: (finiteStateMachine: FsmConsumerInterface) => void | boolean; +}; + +export type SignalConfig = { + signalId?: string; + /** + * @default `No` + */ + receivePrevious?: DebounceType; +} & ( + | { + transition: T['TEventId']; + contextName?: keyof T['TContext']; + callback?: never; + } + | { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (detail: any, fsmInstance: FsmConsumerInterface) => void; + transition?: never; + } +); + +// type helper +export type FsmTypeHelper = Readonly<{ + TState: Exclude; + TEventId: keyof T['stateRecord'][FsmTypeHelper['TState']]['on']; + TActionName: NonNullable['TState']]['entry']>>; + TContext: T['context']; +}>; + +export type FsmConsumerInterface< + T extends FsmTypeHelper = FsmTypeHelper, + TContext extends T['TContext'] = T['TContext'] +> = ReturnType>; diff --git a/demo/es-bench/bench.ts b/demo/es-bench/bench.ts index 70d1564a0..88647b4a8 100644 --- a/demo/es-bench/bench.ts +++ b/demo/es-bench/bench.ts @@ -1,12 +1,13 @@ +const count = 1_000_000; export const bench = (name: string, func: () => void): void => { globalThis.gc?.(); const startMemory = process.memoryUsage.rss(); const startTime = performance.now(); - for (let i = 1_000_000; i; i--) { + for (let i = count; i; i--) { func(); } const duration = performance.now() - startTime; - const runPerSec = Math.round( 1_000_000 / duration * 1000); + const runPerSec = Math.round( count / duration * 1000); const memoryUsage = Math.round((process.memoryUsage.rss() - startMemory) / 10) / 100; console.log(`run ${name} ${runPerSec.toLocaleString()}/s with ${memoryUsage.toLocaleString()}kb`); diff --git a/demo/es-bench/many-bind.ts b/demo/es-bench/many-bind.ts new file mode 100644 index 000000000..f6d62342d --- /dev/null +++ b/demo/es-bench/many-bind.ts @@ -0,0 +1,28 @@ +/* eslint-disable camelcase */ + +import {bench} from './bench.js'; + +function test1(id: string): void { + console.log(id); +} +function test2(id: string): void { + console.log(id); +} +function test3(id: string): void { + console.log(id); +} + +const bind = (id: string) => ({ + id, + test1: test1.bind(null, id), + test2: test2.bind(null, id), + test3: test3.bind(null, id), +} as const); + +function test_bind(): void { + bind('123'); +} + +bench('test_bind', test_bind); + +globalThis.document?.body.append(' Done. Check the console.'); diff --git a/demo/es-bench/object-vs-map.ts b/demo/es-bench/object-vs-map.ts new file mode 100644 index 000000000..e31459f2a --- /dev/null +++ b/demo/es-bench/object-vs-map.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable camelcase */ + +import {bench} from './bench.js'; + +const size = 1_000; + +let userListRecord: Record> = {}; +let userListMap = new Map>(); + +function test_make_map() { + userListMap = new Map>(); + for (let i = size; i; i--) { + const userId = 'user_' + i; + + const user = new Map(); + user.set('user', userId); + user.set('fname', 'ali'); + user.set('lname', 'md'); + user.set('email', 'i@ali.md'); + user.set('token', '1234abcd'); + + userListMap.set(userId, user); + } +} + +function test_make_obj() { + userListRecord = {}; + for (let i = size; i; i--) { + const userId = 'user_' + i; + userListRecord[userId] = { + user: userId, + fname: 'ali', + lname: 'md', + email: 'i@ali.md', + token: '1234abcd', + }; + } +} + +function test_access_map() { + if (userListMap.get('user_' + size / 2)!.get('user') !== 'user_' + size / 2) throw new Error('not_match'); +} + +function test_access_obj() { + if (userListRecord['user_' + size / 2]['user'] !== 'user_' + size / 2) throw new Error('not_match'); +} + +bench('test_make_map_1st', test_make_map); +bench('test_make_obj_1st', test_make_obj); +bench('test_access_map_1st', test_access_map); +bench('test_access_obj_1st', test_access_obj); + +bench('test_make_map_2nd', test_make_map); +bench('test_make_obj_2nd', test_make_obj); +bench('test_access_map_2nd', test_access_map); +bench('test_access_obj_2nd', test_access_obj); + +globalThis.document?.body.append(' Done. Check the console.'); diff --git a/demo/finite-state-machine/index.html b/demo/finite-state-machine/index.html new file mode 100644 index 000000000..497593dbf --- /dev/null +++ b/demo/finite-state-machine/index.html @@ -0,0 +1,13 @@ + + + + + + + FSM + + + + + + diff --git a/demo/finite-state-machine/light-machine.ts b/demo/finite-state-machine/light-machine.ts index 66b1f6af3..8ab860dba 100644 --- a/demo/finite-state-machine/light-machine.ts +++ b/demo/finite-state-machine/light-machine.ts @@ -1,48 +1,143 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; +import {finiteStateMachineConsumer, finiteStateMachineProvider, type FsmTypeHelper} from '@alwatr/fsm'; +import {delay} from '@alwatr/util'; -const lightMachine = new FiniteStateMachine({ - id: 'light-machine', +// Provider +const lightMachineConstructor = finiteStateMachineProvider.defineConstructor('light_machine', { initial: 'green', context: { a: 0, b: 0, }, - states: { + stateRecord: { $all: { + entry: 'action_all_entry', + exit: 'action_all_exit', on: { - POWER_LOST: 'flashingRed', + POWER_LOST: { + target: 'flashingRed', + actions: 'action_all_POWER_LOST', + }, }, }, green: { + entry: 'action_green_entry', + exit: 'action_green_exit', on: { - TIMER: 'yellow', + TIMER: { + target: 'yellow', + actions: 'action_green_TIMER', + }, }, }, yellow: { + entry: 'action_yellow_entry', + exit: 'action_yellow_exit', on: { - TIMER: 'red', + TIMER: { + target: 'red', + actions: 'action_yellow_TIMER', + }, }, }, red: { + entry: 'action_red_entry', + exit: 'action_red_exit', on: { - TIMER: 'green', + TIMER: { + target: 'green', + actions: 'action_red_TIMER', + }, }, }, flashingRed: { + entry: 'action_flashingRed_entry', + exit: 'action_flashingRed_exit', on: { - POWER_BACK: 'green', + POWER_BACK: { + target: 'green', + actions: 'action_flashingRed_POWER_BACK', + }, }, }, }, }); -lightMachine.signal.subscribe((state) => { - console.log('****\nstate: %s, context: %s\n****', state, lightMachine.context); -}, {receivePrevious: 'No'}); +type LightMachine = FsmTypeHelper; -lightMachine.transition('TIMER', {a: 1}); -lightMachine.transition('TIMER', {b: 2}); -lightMachine.transition('TIMER'); -lightMachine.transition('POWER_LOST', {a: 4}); -lightMachine.transition('TIMER', {a: 5, b: 5}); -lightMachine.transition('POWER_BACK', {a: 6}); +// entries actions +finiteStateMachineProvider.defineActions('light_machine', { + 'action_all_entry': (m): void => console.log('$all entry called', m.getState()), + 'action_green_entry': (): void => console.log('green entry called'), + 'action_yellow_entry': (): void => console.log('yellow entry called'), + 'action_red_entry': (): void => console.log('red entry called'), + 'action_flashingRed_entry': (): void => console.log('flashingRed entry called'), +}); + +// exits actions +finiteStateMachineProvider.defineActions('light_machine', { + 'action_all_exit': (): void => console.log('$all exit called'), + 'action_green_exit': (): void => console.log('green exit called'), + 'action_yellow_exit': (): void => console.log('yellow exit called'), + 'action_red_exit': (): void => console.log('red exit called'), + 'action_flashingRed_exit': (): void => console.log('flashingRed exit called'), +}); + +// transition events actions +finiteStateMachineProvider.defineActions('light_machine', { + 'action_all_POWER_LOST': (): void => console.log('$all.POWER_LOST actions called'), + 'action_green_TIMER': (): void => console.log('green.TIMER actions called'), + 'action_yellow_TIMER': (): void => console.log('yellow.TIMER actions called'), + 'action_red_TIMER': (): void => console.log('red.TIMER actions called'), + 'action_flashingRed_POWER_BACK': (): void => console.log('flashingRed.POWER_BACK actions called'), +}); + +finiteStateMachineProvider.defineSignals('light_machine', [ + { + signalId: 'new_content_received', + transition: 'POWER_BACK', + contextName: 'a', + receivePrevious: 'NextCycle', + }, +]); + +// signals +// 'action_ali_signal': (a): void => console.log('ali signal ', a), + +// Consumer +const lightMachineConsumer = finiteStateMachineConsumer('light_machine-50', 'light_machine'); + +lightMachineConsumer.defineSignals([ + { + signalId: 'power_button_click_event', + transition: 'POWER_BACK', + receivePrevious: 'No', + }, + { + signalId: 'jafang', + callback: (signalDetail: Record): void => { + console.log(signalDetail); + }, + receivePrevious: 'NextCycle', + }, + { + callback: (): void => { + console.log('subscribe_callback', lightMachineConsumer.getState()); + }, + receivePrevious: 'NextCycle', + }, +]); + +console.log('start', lightMachineConsumer.getState()); + +await delay(1000); +lightMachineConsumer.transition('TIMER', {a: 1}); +await delay(1000); +lightMachineConsumer.transition('TIMER', {b: 2}); +await delay(1000); +lightMachineConsumer.transition('TIMER'); +await delay(1000); +lightMachineConsumer.transition('POWER_LOST', {a: 4}); +await delay(1000); +lightMachineConsumer.transition('TIMER', {a: 5, b: 5}); +await delay(1000); +lightMachineConsumer.transition('POWER_BACK', {a: 6}); diff --git a/demo/index.html b/demo/index.html index 754be5368..493df6600 100644 --- a/demo/index.html +++ b/demo/index.html @@ -21,6 +21,7 @@
  • Math
  • Icon
  • ES Bench
  • +
  • FSM
  • diff --git a/ui/element/src/index.ts b/ui/element/src/index.ts index 90232ff8e..4d86b7efd 100644 --- a/ui/element/src/index.ts +++ b/ui/element/src/index.ts @@ -9,7 +9,7 @@ export * from './mixins/logging.js'; export * from './mixins/signal.js'; export * from './mixins/toggle.js'; export * from './mixins/unresolved.js'; -export * from './mixins/state-machine.js'; +export * from './mixins/schedule-update-to-frame.js'; export * from './directives/map.js'; diff --git a/ui/element/src/mixins/state-machine.ts b/ui/element/src/mixins/state-machine.ts deleted file mode 100644 index c94b8cfb4..000000000 --- a/ui/element/src/mixins/state-machine.ts +++ /dev/null @@ -1,59 +0,0 @@ -import {untilNextFrame} from '@alwatr/util'; - -import {nothing} from '../lit.js'; - -import type {SignalMixinInterface} from './signal.js'; -import type {FiniteStateMachine} from '@alwatr/fsm'; -import type {Constructor} from '@alwatr/type'; - -export declare class StateMachineMixinInterface extends SignalMixinInterface { - protected stateMachine: TMachine; - protected stateUpdated(state: TMachine['state']): void; - protected render_state_unresolved(): unknown; - protected render_state_resolving(): unknown; -} - -export function StateMachineMixin, TMachine extends FiniteStateMachine>( - stateMachine: TMachine, - superClass: T, -): Constructor> & T { - class StateMachineMixinClass extends superClass { - protected stateMachine = stateMachine; - - protected render_state_unresolved(): unknown { - return nothing; - } - protected render_state_resolving(): unknown { - return nothing; - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - constructor(...args: any[]) { - super(...args); - this.stateUpdated = this.stateUpdated.bind(this); - } - - override connectedCallback(): void { - super.connectedCallback(); - this.stateMachine.transition('CONNECTED'); - this._signalListenerList.push( - this.stateMachine.signal.subscribe(this.stateUpdated, {receivePrevious: 'NextCycle'}), - ); - } - - /** - * Subscribe to this.stateMachine.signal event. - */ - protected stateUpdated(state: TMachine['state']): void { - this.setAttribute('state', state.to); - this.requestUpdate(); - } - - protected override async scheduleUpdate(): Promise { - await untilNextFrame(); - super.scheduleUpdate(); - } - } - - return StateMachineMixinClass as unknown as Constructor> & T; -} diff --git a/uniquely/com-pwa/src/content/l18e-fa.json b/uniquely/com-pwa/src/content/l18e-fa.json index e73c81cc7..c1b9534d0 100644 --- a/uniquely/com-pwa/src/content/l18e-fa.json +++ b/uniquely/com-pwa/src/content/l18e-fa.json @@ -14,6 +14,9 @@ "page_404_under_develope": "دردست ساخت", "page_404_under_develope_description": "برنامه نویسان سخت مشغول کارند...\nاندکی صبر سحر نزدیک است...\nدرضمن اگر رو من بزنی یک سفارش الکی ثبت می‌شود\nپس لطفا نزن!", + "fetch_failed_headline": "خطا!", + "fetch_failed_description": "خطا در اتصال به اینترنت! لطفا مجدد تلاش کنید.", + "page_order_detail_not_found": "سفارش پیدا نشد!", "page_order_tracking_not_found": "سفارش پیدا نشد!", diff --git a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts index 0d3adb25d..979698a87 100644 --- a/uniquely/com-pwa/src/manager/context-provider/order-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/order-storage.ts @@ -1,43 +1,96 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {contextConsumer, DispatchOptions, requestableContextProvider} from '@alwatr/signal'; import {config} from '../../config.js'; -import { - orderStorageContextConsumer, - userContextConsumer, -} from '../context.js'; import {logger} from '../logger.js'; -export const fetchOrderStorage = async (): Promise => { - logger.logMethod('fetchOrderStorage'); +import type {AlwatrDocumentStorage, User} from '@alwatr/type'; +import type {Order} from '@alwatr/type/customer-order-management.js'; + +const orderStorageContextProvider = + requestableContextProvider.bind>('order-storage-context'); + +const userContextConsumer = contextConsumer.bind('user-context'); + +orderStorageContextProvider.setProvider(async () => { + logger.logMethod('requestOrderStorageContext'); + let context = orderStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; const userContext = userContextConsumer.getValue() ?? (await userContextConsumer.untilChange()); + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/order-list/', + queryParameters: { + userId: userContext.id, + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + orderStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = await serviceRequest(fetchOption) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestOrderStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestOrderStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; + } try { - await fetchContext( - orderStorageContextConsumer.id, - { - ...config.fetchContextOptions, - url: config.api + '/order-list/', - queryParameters: { - userId: userContext.id, - }, - }, - {debounce: 'NextCycle'}, - ); + fetchOption.cacheStrategy = 'update_cache'; + const response = await serviceRequest(fetchOption) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + orderStorageContextProvider.setValue(context, dispatchOptions); } catch (err) { - // TODO: refactor - logger.error('fetchOrderStorage', 'fetch_failed', err); - await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: orderStorageContextConsumer.getValue() == null ? -1 : 5_000, - }); - if (response.actionButton) { - await fetchOrderStorage(); - } + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + orderStorageContextProvider.setValue(context, dispatchOptions); + return; } -}; +}); diff --git a/uniquely/com-pwa/src/manager/context-provider/price-storage.ts b/uniquely/com-pwa/src/manager/context-provider/price-storage.ts index 7cdddc4f3..3406f0f54 100644 --- a/uniquely/com-pwa/src/manager/context-provider/price-storage.ts +++ b/uniquely/com-pwa/src/manager/context-provider/price-storage.ts @@ -1,50 +1,182 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {DispatchOptions, requestableContextProvider} from '@alwatr/signal'; +import {ProductPrice} from '@alwatr/type/src/customer-order-management.js'; import {config} from '../../config.js'; import {logger} from '../logger.js'; -export const fetchPriceStorage = async (productStorageName = 'tile'): Promise => { - logger.logMethod('fetchPriceStorage'); +import type {AlwatrDocumentStorage} from '@alwatr/type'; + +const productPriceStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('product-price-context'); + +const finalProductPriceStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('final-product-price-context'); + +productPriceStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestProductPriceStorageContext'); + let context = productPriceStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/price-list/', + queryParameters: { + name: config.priceListName.replace('${productStorage}', args.productPriceStorageName), + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestProductPriceStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestProductPriceStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + + try { + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + productPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } +}); + +finalProductPriceStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestFinalProductPriceStorageContext'); + let context = finalProductPriceStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/price-list/', + queryParameters: { + name: config.finalPriceListName.replace('${productStorage}', args.productPriceStorageName), + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestFinalProductPriceStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestFinalProductPriceStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } try { - void await Promise.all([ - fetchContext( - `price-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/price-list/', - queryParameters: { - name: config.priceListName.replace('${productStorage}', productStorageName), - }, - }, - {debounce: 'NextCycle'}, - ), - fetchContext( - `final-price-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/price-list/', - queryParameters: { - name: config.finalPriceListName.replace('${productStorage}', productStorageName), - }, - }, - {debounce: 'NextCycle'}, - ), - ]); + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); } catch (err) { - // TODO: refactor - logger.error('provideProductStorageContext', 'fetch_failed', err); - await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: -1, - }); - if (response.actionButton) { - await fetchPriceStorage(productStorageName); - } - } -}; + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + finalProductPriceStorageContextProvider.setValue(context, dispatchOptions); + return; + } +}); diff --git a/uniquely/com-pwa/src/manager/context-provider/product-price-storage.ts b/uniquely/com-pwa/src/manager/context-provider/product-price-storage.ts new file mode 100644 index 000000000..699fa527e --- /dev/null +++ b/uniquely/com-pwa/src/manager/context-provider/product-price-storage.ts @@ -0,0 +1,95 @@ +import {serviceRequest, type FetchOptions} from '@alwatr/fetch'; +import {DispatchOptions, requestableContextProvider} from '@alwatr/signal'; +import {Product} from '@alwatr/type/src/customer-order-management.js'; + +import {config} from '../../config.js'; +import {logger} from '../logger.js'; + +import type {AlwatrDocumentStorage} from '@alwatr/type'; + +const productStorageContextProvider = requestableContextProvider.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); + +productStorageContextProvider.setProvider(async (args) => { + logger.logMethod('requestProductStorageContext'); + let context = productStorageContextProvider.getValue(); + + if (context.state === 'pending' || context.state === 'reloading') return; + + const dispatchOptions: Partial = {debounce: 'NextCycle'}; + const fetchOption: FetchOptions = { + ...config.fetchContextOptions, + url: config.api + '/product-list/', + queryParameters: { + storage: args.productStorageName, + }, + }; + + if (context.state === 'initial') { + context = {state: 'pending'}; + productStorageContextProvider.setValue(context, dispatchOptions); + try { + fetchOption.cacheStrategy = 'cache_only'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + context = { + state: 'reloading', + content: response, + }; + productStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + if ((err as Error).message === 'fetch_cache_not_found') { + logger.logOther('requestProductStorageContext:', 'fetch_cache_not_found'); + } + else { + logger.error('requestProductStorageContext', 'fetch_failed', err); + context = {state: 'error'}; + productStorageContextProvider.setValue(context, dispatchOptions); + return; + } + } + } + + if (navigator.onLine === false) { + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productStorageContextProvider.setValue(context, dispatchOptions); + return; + } + + try { + fetchOption.cacheStrategy = 'update_cache'; + const response = (await serviceRequest(fetchOption)) as AlwatrDocumentStorage; + if ( + context.content != null && + response.meta?.lastUpdated != undefined && + response.meta.lastUpdated === context.content.meta?.lastUpdated + ) { + // no changed + context = { + state: 'complete', + content: context.content, + }; + } + else { + context = { + state: 'complete', + content: response, + }; + } + productStorageContextProvider.setValue(context, dispatchOptions); + } + catch (err) { + logger.error('fetchContext', 'fetch_failed', err); + context = { + state: 'error', + content: context.content, // maybe offline exist + }; + productStorageContextProvider.setValue(context, dispatchOptions); + return; + } +}); diff --git a/uniquely/com-pwa/src/manager/context-provider/product-storage.ts b/uniquely/com-pwa/src/manager/context-provider/product-storage.ts deleted file mode 100644 index f471751c4..000000000 --- a/uniquely/com-pwa/src/manager/context-provider/product-storage.ts +++ /dev/null @@ -1,37 +0,0 @@ -import {fetchContext} from '@alwatr/fetch'; -import {l18eReadyPromise} from '@alwatr/i18n'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; - -import {config} from '../../config.js'; -import {logger} from '../logger.js'; - -export const fetchProductStorage = async (productStorageName = 'tile'): Promise => { - logger.logMethod('fetchProductStorage'); - - try { - await fetchContext( - `product-storage-${productStorageName}-context`, - { - ...config.fetchContextOptions, - url: config.api + '/product-list/', - queryParameters: { - storage: productStorageName, - }, - }, - {debounce: 'NextCycle'}, - ); - } - catch (err) { - // TODO: refactor - logger.error('provideProductStorageContext', 'fetch_failed', err); - await l18eReadyPromise; - const response = await snackbarSignalTrigger.requestWithResponse({ - messageKey: 'fetch_failed', - actionLabelKey: 'retry', - duration: -1, - }); - if (response.actionButton) { - await fetchProductStorage(productStorageName); - } - } -}; diff --git a/uniquely/com-pwa/src/manager/context-provider/user.ts b/uniquely/com-pwa/src/manager/context-provider/user.ts index 744baff28..c929157d3 100644 --- a/uniquely/com-pwa/src/manager/context-provider/user.ts +++ b/uniquely/com-pwa/src/manager/context-provider/user.ts @@ -1,9 +1,11 @@ +import {contextProvider} from '@alwatr/signal'; +import {User} from '@alwatr/type'; import {getLocalStorageItem} from '@alwatr/util'; -import {userContextProvider} from '../context.js'; +const userContextProvider = contextProvider.bind('user-context'); // demo -userContextProvider.setValue(getLocalStorageItem('user-context', { +userContextProvider.setValue(getLocalStorageItem(userContextProvider.id, { id: 'demo-123', fullName: 'Demo User', })); diff --git a/uniquely/com-pwa/src/manager/context.ts b/uniquely/com-pwa/src/manager/context.ts index e92cc84a5..575583f92 100644 --- a/uniquely/com-pwa/src/manager/context.ts +++ b/uniquely/com-pwa/src/manager/context.ts @@ -6,26 +6,10 @@ import { import {PageHomeContent} from '../type.js'; -import type {AlwatrDocumentStorage, User} from '@alwatr/type'; -import type {Product, Order, ProductPrice} from '@alwatr/type/customer-order-management.js'; +import type {Order} from '@alwatr/type/customer-order-management.js'; export * from '@alwatr/pwa-helper/context.js'; -export const productStorageContextConsumer = - contextConsumer.bind>('product-storage-tile-context'); - -export const priceStorageContextConsumer = - contextConsumer.bind>('price-storage-tile-context'); - -export const finalPriceStorageContextConsumer = - contextConsumer.bind>('final-price-storage-tile-context'); - -export const orderStorageContextConsumer = - contextConsumer.bind>('order-storage-context'); - -export const userContextProvider = contextProvider.bind('user-context'); -export const userContextConsumer = contextConsumer.bind(userContextProvider.id); - export const homePageContentContextProvider = contextProvider.bind('home-page-content-context'); export const homePageContentContextConsumer = diff --git a/uniquely/com-pwa/src/manager/controller/new-order.ts b/uniquely/com-pwa/src/manager/controller/new-order.ts index 3b72b0c89..a18c3a740 100644 --- a/uniquely/com-pwa/src/manager/controller/new-order.ts +++ b/uniquely/com-pwa/src/manager/controller/new-order.ts @@ -1,330 +1,278 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; -import {orderInfoSchema, tileQtyStep} from '@alwatr/type/customer-order-management.js'; -import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; -import {getLocalStorageItem} from '@alwatr/util'; -import {validator} from '@alwatr/validator'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {message} from '@alwatr/i18n'; +// import {redirect} from '@alwatr/router'; +// import {eventListener} from '@alwatr/signal'; +// import {orderInfoSchema, tileQtyStep} from '@alwatr/type/customer-order-management.js'; +// import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +// import {getLocalStorageItem} from '@alwatr/util'; +// import {validator} from '@alwatr/validator'; -import {fetchPriceStorage} from '../context-provider/price-storage.js'; -import {fetchProductStorage} from '../context-provider/product-storage.js'; -import { - finalPriceStorageContextConsumer, - priceStorageContextConsumer, - productStorageContextConsumer, - scrollToTopCommand, - submitOrderCommandTrigger, - topAppBarContextProvider, -} from '../context.js'; -import {logger} from '../logger.js'; +// import {fetchPriceStorage} from '../context-provider/price-storage.js'; +// import {fetchProductStorage} from '../context-provider/product-storage.js'; +// import { +// finalPriceStorageContextConsumer, +// priceStorageContextConsumer, +// productStorageContextConsumer, +// scrollToTopCommand, +// submitOrderCommandTrigger, +// topAppBarContextProvider, +// } from '../context.js'; +// import {logger} from '../logger.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Product, ProductPrice, OrderDraft, OrderItem} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Product, ProductPrice, OrderDraft, OrderItem} from '@alwatr/type/customer-order-management.js'; -export const pageNewOrderStateMachine = new FiniteStateMachine({ - id: 'page-order-detail', - initial: 'unresolved', - context: { - registeredOrderId: '', - order: getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}), - productStorage: | null>null, - priceStorage: | null>null, - finalPriceStorage: | null>null, - }, - states: { - $all: { - on: { - CONNECTED: '$self', - PARTIAL_LOAD: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - CONTEXT_LOADED: 'edit', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - CONTEXT_LOADED: 'edit', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'edit', - }, - }, - edit: { - on: { - SELECT_PRODUCT: 'selectProduct', - EDIT_SHIPPING: 'shippingForm', - SUBMIT: 'review', - QTY_UPDATE: '$self', - }, - }, - selectProduct: { - on: { - SUBMIT: 'edit', - }, - }, - shippingForm: { - on: { - SUBMIT: 'edit', - }, - }, - review: { - on: { - BACK: 'edit', - VALIDATION_FAILED: 'edit', - FINAL_SUBMIT: 'submitting', - }, - }, - submitting: { - on: { - SUBMIT_SUCCESS: 'submitSuccess', - SUBMIT_FAILED: 'submitFailed', - }, - }, - submitSuccess: { - on: { - NEW_ORDER: 'edit', - }, - }, - submitFailed: { - on: { - FINAL_SUBMIT: 'submitting', - }, - }, - }, -}); +// export const pageNewOrderStateMachine = new FiniteStateMachine({ +// id: 'page-order-detail', +// initial: 'unresolved', +// context: { +// registeredOrderId: '', +// order: getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}), +// productStorage: | null>null, +// priceStorage: | null>null, +// finalPriceStorage: | null>null, +// }, +// stateRecord: { +// $all: { +// on: { +// CONNECTED: '$self', +// PARTIAL_LOAD: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// CONTEXT_LOADED: 'edit', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// CONTEXT_LOADED: 'edit', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'edit', +// }, +// }, +// edit: { +// on: { +// SELECT_PRODUCT: 'selectProduct', +// EDIT_SHIPPING: 'shippingForm', +// SUBMIT: 'review', +// QTY_UPDATE: '$self', +// }, +// }, +// selectProduct: { +// on: { +// SUBMIT: 'edit', +// }, +// }, +// shippingForm: { +// on: { +// SUBMIT: 'edit', +// }, +// }, +// review: { +// on: { +// BACK: 'edit', +// VALIDATION_FAILED: 'edit', +// FINAL_SUBMIT: 'submitting', +// }, +// }, +// submitting: { +// on: { +// SUBMIT_SUCCESS: 'submitSuccess', +// SUBMIT_FAILED: 'submitFailed', +// }, +// }, +// submitSuccess: { +// on: { +// NEW_ORDER: 'edit', +// }, +// }, +// submitFailed: { +// on: { +// FINAL_SUBMIT: 'submitting', +// }, +// }, +// }, +// }); -export const buttons = { - back: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: pageNewOrderStateMachine.config.id + '_back_click_event', - }, - backToHome: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: 'back_to_home_click_event', - }, - editItems: { - icon: 'create-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_items_click_event', - }, - submit: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_click_event', - }, - edit: { - icon: 'create-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_click_event', - }, - submitFinal: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_final_click_event', - }, - submitShippingForm: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_submit_shipping_form_click_event', - }, - editShippingForm: { - icon: 'checkmark-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_edit_shipping_form_click_event', - }, - newOrder: { - icon: 'add-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_new_order_click_event', - }, - detail: { - icon: 'information-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_detail_click_event', - }, - tracking: { - icon: 'chatbox-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_tracking_click_event', - }, - retry: { - icon: 'reload-outline', - clickSignalId: pageNewOrderStateMachine.config.id + '_retry_click_event', - }, -} as const; +// pageNewOrderStateMachine.signal.subscribe(async (state) => { +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// // topAppBarContextProvider.setValue({ +// // headlineKey: 'loading', +// // }); +// if (productStorageContextConsumer.getValue() == null) { +// fetchProductStorage(); +// } +// if ( +// priceStorageContextConsumer.getValue() == null || +// finalPriceStorageContextConsumer.getValue() == null +// ) { +// fetchPriceStorage(); +// } +// break; +// } -pageNewOrderStateMachine.signal.subscribe(async (state) => { - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if ( - priceStorageContextConsumer.getValue() == null || - finalPriceStorageContextConsumer.getValue() == null - ) { - fetchPriceStorage(); - } - break; - } +// case 'PARTIAL_LOAD': { +// if (Object.values(pageNewOrderStateMachine.context).indexOf(null) === -1) { +// pageNewOrderStateMachine.transition('CONTEXT_LOADED'); +// } +// break; +// } - case 'PARTIAL_LOAD': { - if (Object.values(pageNewOrderStateMachine.context).indexOf(null) === -1) { - pageNewOrderStateMachine.transition('CONTEXT_LOADED'); - } - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_new_order_headline', +// startIcon: buttons.backToHome, +// }); +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_new_order_headline', - startIcon: buttons.backToHome, - }); - break; - } +// case 'NEW_ORDER': { +// pageNewOrderStateMachine.context.registeredOrderId = ''; +// break; +// } - case 'NEW_ORDER': { - pageNewOrderStateMachine.context.registeredOrderId = ''; - break; - } +// case 'FINAL_SUBMIT': { +// const order = await submitOrderCommandTrigger.requestWithResponse(pageNewOrderStateMachine.context.order); +// if (order == null) { +// pageNewOrderStateMachine.transition('SUBMIT_FAILED'); +// return; +// } +// // else +// pageNewOrderStateMachine.context.registeredOrderId = order.id; +// pageNewOrderStateMachine.transition('SUBMIT_SUCCESS'); +// break; +// } - case 'FINAL_SUBMIT': { - const order = await submitOrderCommandTrigger.requestWithResponse(pageNewOrderStateMachine.context.order); - if (order == null) { - pageNewOrderStateMachine.transition('SUBMIT_FAILED'); - return; - } - // else - pageNewOrderStateMachine.context.registeredOrderId = order.id; - pageNewOrderStateMachine.transition('SUBMIT_SUCCESS'); - break; - } +// case 'SUBMIT_SUCCESS': { +// localStorage.removeItem('draft-order-x2'); +// pageNewOrderStateMachine.context.order = getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}); +// break; +// } +// } +// }); - case 'SUBMIT_SUCCESS': { - localStorage.removeItem('draft-order-x2'); - pageNewOrderStateMachine.context.order = getLocalStorageItem('draft-order-x2', {id: 'new', status: 'draft'}); - break; - } - } -}); +// pageNewOrderStateMachine.signal.subscribe(async (state) => { +// localStorage.setItem('draft-order-x2', JSON.stringify(pageNewOrderStateMachine.context.order)); -pageNewOrderStateMachine.signal.subscribe(async (state) => { - localStorage.setItem('draft-order-x2', JSON.stringify(pageNewOrderStateMachine.context.order)); +// if (state.target != 'shippingForm' && state.target != state.from) { +// scrollToTopCommand.request({}); +// } - if (state.to != 'shippingForm' && state.to != state.from) { - scrollToTopCommand.request({}); - } +// if ( +// state.target === 'edit' && +// state.from != 'selectProduct' && +// !pageNewOrderStateMachine.context.order?.itemList?.length +// ) { +// pageNewOrderStateMachine.transition('SELECT_PRODUCT'); +// } - if ( - state.to === 'edit' && - state.from != 'selectProduct' && - !pageNewOrderStateMachine.context.order?.itemList?.length - ) { - pageNewOrderStateMachine.transition('SELECT_PRODUCT'); - } +// else if (state.target === 'edit' || state.target === 'review') { +// const order = pageNewOrderStateMachine.context.order; +// let totalPrice = 0; +// let finalTotalPrice = 0; +// for (const item of order.itemList ?? []) { +// totalPrice += item.price * item.qty * tileQtyStep; +// finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; +// } +// order.totalPrice = Math.round(totalPrice); +// order.finalTotalPrice = Math.round(finalTotalPrice); +// } - else if (state.to === 'edit' || state.to === 'review') { - const order = pageNewOrderStateMachine.context.order; - let totalPrice = 0; - let finalTotalPrice = 0; - for (const item of order.itemList ?? []) { - totalPrice += item.price * item.qty * tileQtyStep; - finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; - } - order.totalPrice = Math.round(totalPrice); - order.finalTotalPrice = Math.round(finalTotalPrice); - } +// if (state.target === 'review') { +// try { +// validator(orderInfoSchema, pageNewOrderStateMachine.context.order, true); +// } +// catch (err) { +// pageNewOrderStateMachine.transition('VALIDATION_FAILED'); +// const _err = err as (Error & {cause?: Record}); +// logger.incident('SUBMIT', _err.name, 'validation failed', _err); +// if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { +// snackbarSignalTrigger.request({ +// message: message('page_new_order_shipping_info_not_valid_message'), +// }); +// } +// else { +// snackbarSignalTrigger.request({ +// message: message('page_new_order_order_not_valid_message'), +// }); +// } +// } +// } +// }); - if (state.to === 'review') { - try { - validator(orderInfoSchema, pageNewOrderStateMachine.context.order, true); - } - catch (err) { - pageNewOrderStateMachine.transition('VALIDATION_FAILED'); - const _err = err as (Error & {cause?: Record}); - logger.incident('SUBMIT', _err.name, 'validation failed', _err); - if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { - snackbarSignalTrigger.request({ - messageKey: 'page_new_order_shipping_info_not_valid_message', - }); - } - else { - snackbarSignalTrigger.request({ - messageKey: 'page_new_order_order_not_valid_message', - }); - } - } - } -}); +// productStorageContextConsumer.subscribe((productStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {productStorage}); +// }); +// priceStorageContextConsumer.subscribe((priceStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {priceStorage}); +// }); +// finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { +// pageNewOrderStateMachine.transition('PARTIAL_LOAD', {finalPriceStorage}); +// }); -productStorageContextConsumer.subscribe((productStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {productStorage}); -}); -priceStorageContextConsumer.subscribe((priceStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {priceStorage}); -}); -finalPriceStorageContextConsumer.subscribe((finalPriceStorage) => { - pageNewOrderStateMachine.transition('PARTIAL_LOAD', {finalPriceStorage}); -}); +// eventListener.subscribe(buttons.submit.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SUBMIT'); +// }); -eventListener.subscribe(buttons.submit.clickSignalId, () => { - pageNewOrderStateMachine.transition('SUBMIT'); -}); +// eventListener.subscribe(buttons.edit.clickSignalId, () => { +// pageNewOrderStateMachine.transition('BACK'); +// }); -eventListener.subscribe(buttons.edit.clickSignalId, () => { - pageNewOrderStateMachine.transition('BACK'); -}); +// eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { +// pageNewOrderStateMachine.transition('FINAL_SUBMIT'); +// }); -eventListener.subscribe(buttons.submitFinal.clickSignalId, () => { - pageNewOrderStateMachine.transition('FINAL_SUBMIT'); -}); +// eventListener.subscribe(buttons.editItems.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SELECT_PRODUCT'); +// }); -eventListener.subscribe(buttons.editItems.clickSignalId, () => { - pageNewOrderStateMachine.transition('SELECT_PRODUCT'); -}); +// eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { +// pageNewOrderStateMachine.transition('EDIT_SHIPPING'); +// }); -eventListener.subscribe(buttons.editShippingForm.clickSignalId, () => { - pageNewOrderStateMachine.transition('EDIT_SHIPPING'); -}); +// eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { +// pageNewOrderStateMachine.transition('SUBMIT'); +// }); -eventListener.subscribe(buttons.submitShippingForm.clickSignalId, () => { - pageNewOrderStateMachine.transition('SUBMIT'); -}); +// eventListener.subscribe(buttons.tracking.clickSignalId, () => { +// const orderId = pageNewOrderStateMachine.context.registeredOrderId; +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect({sectionList: ['order-tracking', orderId]}); +// }); -eventListener.subscribe(buttons.tracking.clickSignalId, () => { - const orderId = pageNewOrderStateMachine.context.registeredOrderId; - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-tracking', orderId]}); -}); +// eventListener.subscribe(buttons.detail.clickSignalId, () => { +// const orderId = pageNewOrderStateMachine.context.registeredOrderId; +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect({sectionList: ['order-detail', orderId]}); +// }); -eventListener.subscribe(buttons.detail.clickSignalId, () => { - const orderId = pageNewOrderStateMachine.context.registeredOrderId; - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect({sectionList: ['order-detail', orderId]}); -}); +// eventListener.subscribe(buttons.newOrder.clickSignalId, () => { +// pageNewOrderStateMachine.transition('NEW_ORDER'); +// redirect('/new-order/'); +// }); -eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - pageNewOrderStateMachine.transition('NEW_ORDER'); - redirect('/new-order/'); -}); +// eventListener.subscribe(buttons.retry.clickSignalId, async () => { +// pageNewOrderStateMachine.transition('FINAL_SUBMIT'); +// }); -eventListener.subscribe(buttons.retry.clickSignalId, async () => { - pageNewOrderStateMachine.transition('FINAL_SUBMIT'); -}); - -export const qtyUpdate = (orderItem: OrderItem, add: number): void => { - const qty = orderItem.qty + add; - if (qty <= 0) return; - orderItem.qty = qty; - pageNewOrderStateMachine.transition('QTY_UPDATE'); -}; -eventListener.subscribe>('order_item_qty_add', (event) => { - qtyUpdate(event.detail, 1); -}); -eventListener.subscribe>('order_item_qty_remove', (event) => { - qtyUpdate(event.detail, -1); -}); +// export const qtyUpdate = (orderItem: OrderItem, add: number): void => { +// const qty = orderItem.qty + add; +// if (qty <= 0) return; +// orderItem.qty = qty; +// pageNewOrderStateMachine.transition('QTY_UPDATE'); +// }; +// eventListener.subscribe>('order_item_qty_add', (event) => { +// qtyUpdate(event.detail, 1); +// }); +// eventListener.subscribe>('order_item_qty_remove', (event) => { +// qtyUpdate(event.detail, -1); +// }); diff --git a/uniquely/com-pwa/src/manager/controller/order-detail.ts b/uniquely/com-pwa/src/manager/controller/order-detail.ts index 2fbe53e81..077561821 100644 --- a/uniquely/com-pwa/src/manager/controller/order-detail.ts +++ b/uniquely/com-pwa/src/manager/controller/order-detail.ts @@ -1,140 +1,140 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {redirect} from '@alwatr/router'; +// import {eventListener} from '@alwatr/signal'; -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {fetchProductStorage} from '../context-provider/product-storage.js'; -import {orderStorageContextConsumer, productStorageContextConsumer, topAppBarContextProvider} from '../context.js'; +// import {fetchOrderStorage} from '../context-provider/order-storage.js'; +// import {fetchProductStorage} from '../context-provider/product-storage.js'; +// import {orderStorageContextConsumer, productStorageContextConsumer, topAppBarContextProvider} from '../context.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Order, Product} from '@alwatr/type/customer-order-management.js'; -export const pageOrderDetailStateMachine = new FiniteStateMachine({ - id: 'page_order_detail', - initial: 'unresolved', - context: { - orderId: null, - orderStorage: | null>null, - productStorage: | null> null, - }, - states: { - $all: { - on: { - SHOW_DETAIL: '$self', - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - detail: { - on: { - REQUEST_UPDATE: 'reloading', - INVALID_ORDER: 'notFound', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - notFound: { - on: { - CONTEXT_LOADED: 'detail', - }, - }, - }, -}); +// export const pageOrderDetailStateMachine = new FiniteStateMachine({ +// id: 'page_order_detail', +// initial: 'unresolved', +// context: { +// orderId: null, +// orderStorage: | null>null, +// productStorage: | null> null, +// }, +// stateRecord: { +// $all: { +// on: { +// SHOW_DETAIL: '$self', +// CONNECTED: '$self', +// CONTEXT_LOADED: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// detail: { +// on: { +// REQUEST_UPDATE: 'reloading', +// INVALID_ORDER: 'notFound', +// }, +// }, +// reloading: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// notFound: { +// on: { +// CONTEXT_LOADED: 'detail', +// }, +// }, +// }, +// }); -const buttons = { - backToOrderList: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: pageOrderDetailStateMachine.config.id + '_back_to_order_list_click_event', - }, - reload: { - icon: 'reload-outline', - flipRtl: true, - clickSignalId: pageOrderDetailStateMachine.config.id + '_reload_click_event', - }, -} as const; +// const buttons = { +// backToOrderList: { +// icon: 'arrow-back-outline', +// flipRtl: true, +// clickSignalId: pageOrderDetailStateMachine.config.id + '_back_to_order_list_click_event', +// }, +// reload: { +// icon: 'reload-outline', +// flipRtl: true, +// clickSignalId: pageOrderDetailStateMachine.config.id + '_reload_click_event', +// }, +// } as const; -pageOrderDetailStateMachine.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderDetailFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (productStorageContextConsumer.getValue() == null) { - fetchProductStorage(); - } - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } +// pageOrderDetailStateMachine.signal.subscribe(async (state) => { +// logger.logMethodArgs('pageOrderDetailFsm.changed', state); +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// topAppBarContextProvider.setValue({ +// headlineKey: 'loading', +// }); +// if (productStorageContextConsumer.getValue() == null) { +// fetchProductStorage(); +// } +// if (orderStorageContextConsumer.getValue() == null) { +// fetchOrderStorage(); +// } +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToOrderList, - endIconList: [buttons.reload], - }); - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_order_list_headline', +// startIcon: buttons.backToOrderList, +// endIconList: [buttons.reload], +// }); +// break; +// } - case 'REQUEST_UPDATE': { - await fetchOrderStorage(); // if not changed signal not fired! - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - break; - } - } +// case 'REQUEST_UPDATE': { +// await fetchOrderStorage(); // if not changed signal not fired! +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// break; +// } +// } - if (state.to === 'loading') { - if ( - pageOrderDetailStateMachine.context.orderStorage != null && - pageOrderDetailStateMachine.context.productStorage != null - ) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } - } -}); +// if (state.target === 'loading') { +// if ( +// pageOrderDetailStateMachine.context.orderStorage != null && +// pageOrderDetailStateMachine.context.productStorage != null +// ) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// } +// }); -productStorageContextConsumer.subscribe((productStorage) => { - pageOrderDetailStateMachine.context.productStorage = productStorage; - if (pageOrderDetailStateMachine.context.orderStorage != null) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } -}); +// productStorageContextConsumer.subscribe((productStorage) => { +// pageOrderDetailStateMachine.context.productStorage = productStorage; +// if (pageOrderDetailStateMachine.context.orderStorage != null) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// }); -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderDetailStateMachine.context.orderStorage = orderStorage; - if (pageOrderDetailStateMachine.context.productStorage != null) { - pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); - } -}); +// orderStorageContextConsumer.subscribe((orderStorage) => { +// pageOrderDetailStateMachine.context.orderStorage = orderStorage; +// if (pageOrderDetailStateMachine.context.productStorage != null) { +// pageOrderDetailStateMachine.transition('CONTEXT_LOADED'); +// } +// }); -eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { - redirect({ - sectionList: ['order-list'], - }); -}); +// eventListener.subscribe(buttons.backToOrderList.clickSignalId, () => { +// redirect({ +// sectionList: ['order-list'], +// }); +// }); -eventListener.subscribe(buttons.reload.clickSignalId, () => { - pageOrderDetailStateMachine.transition('REQUEST_UPDATE'); -}); +// eventListener.subscribe(buttons.reload.clickSignalId, () => { +// pageOrderDetailStateMachine.transition('REQUEST_UPDATE'); +// }); diff --git a/uniquely/com-pwa/src/manager/controller/order-list.ts b/uniquely/com-pwa/src/manager/controller/order-list.ts deleted file mode 100644 index a6d080903..000000000 --- a/uniquely/com-pwa/src/manager/controller/order-list.ts +++ /dev/null @@ -1,138 +0,0 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {redirect} from '@alwatr/router'; -import {eventListener} from '@alwatr/signal'; - -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; - -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/customer-order-management.js'; - -export const pageOrderListStateMachine = new FiniteStateMachine({ - id: 'page-order-list', - initial: 'unresolved', - context: { - orderStorage: | null>null, - }, - states: { - $all: { - on: { - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'list', - }, - }, - list: { - on: { - REQUEST_UPDATE: 'reloading', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'list', - }, - }, - }, -} as const); - -export const buttons = { - backToHome: { - icon: 'arrow-back-outline', - flipRtl: true, - clickSignalId: 'back_to_home_click_event', - }, - reload: { - icon: 'reload-outline', - // flipRtl: true, - clickSignalId: pageOrderListStateMachine.config.id + '_reload_click_event', - }, - newOrder: { - icon: 'add-outline', - clickSignalId: pageOrderListStateMachine.config.id + '_new_order_click_event', - }, - orderDetail: { - clickSignalId: pageOrderListStateMachine.config.id + '_order_detail_click_event', - }, -} as const; - -pageOrderListStateMachine.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderListFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } - - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - break; - } - - case 'REQUEST_UPDATE': { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - await fetchOrderStorage(); - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_list_headline', - startIcon: buttons.backToHome, - endIconList: [buttons.newOrder, buttons.reload], - }); - pageOrderListStateMachine.transition('CONTEXT_LOADED'); - break; - } - } - - if (state.to === 'loading') { - if (pageOrderListStateMachine.context.orderStorage != null) { - pageOrderListStateMachine.transition('CONTEXT_LOADED'); - } - } -}); - -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderListStateMachine.transition('CONTEXT_LOADED', {orderStorage}); -}); - -eventListener.subscribe(buttons.reload.clickSignalId, () => { - pageOrderListStateMachine.transition('REQUEST_UPDATE'); -}); - -eventListener.subscribe(buttons.newOrder.clickSignalId, () => { - redirect({ - sectionList: ['new-order'], - }); -}); - -eventListener.subscribe>(buttons.orderDetail.clickSignalId, (event) => { - redirect({ - sectionList: ['order-detail', event.detail.id], - }); -}); diff --git a/uniquely/com-pwa/src/manager/controller/order-tracking.ts b/uniquely/com-pwa/src/manager/controller/order-tracking.ts index 18a72dded..dfbf7ef97 100644 --- a/uniquely/com-pwa/src/manager/controller/order-tracking.ts +++ b/uniquely/com-pwa/src/manager/controller/order-tracking.ts @@ -1,101 +1,101 @@ -import {FiniteStateMachine} from '@alwatr/fsm'; -import {eventListener} from '@alwatr/signal'; +// import {FiniteStateMachine} from '@alwatr/fsm'; +// import {eventListener} from '@alwatr/signal'; -import {fetchOrderStorage} from '../context-provider/order-storage.js'; -import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; +// import {fetchOrderStorage} from '../context-provider/order-storage.js'; +// import {orderStorageContextConsumer, topAppBarContextProvider} from '../context.js'; -import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; -import type {Order} from '@alwatr/type/customer-order-management.js'; +// import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +// import type {Order} from '@alwatr/type/customer-order-management.js'; -export const pageOrderTrackingFsm = new FiniteStateMachine({ - id: 'page-order-tracking', - initial: 'unresolved', - context: { - orderId: null, - orderStorage: | null>null, - }, - states: { - $all: { - on: { - SHOW_TRACKING: '$self', - CONNECTED: '$self', - CONTEXT_LOADED: '$self', - }, - }, - unresolved: { - on: { - IMPORT: 'resolving', - }, - }, - resolving: { - on: { - CONNECTED: 'loading', - }, - }, - loading: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - tracking: { - on: { - REQUEST_UPDATE: 'reloading', - INVALID_ORDER: 'notFound', - }, - }, - reloading: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - notFound: { - on: { - CONTEXT_LOADED: 'tracking', - }, - }, - }, -}); +// export const pageOrderTrackingFsm = new FiniteStateMachine({ +// id: 'page-order-tracking', +// initial: 'unresolved', +// context: { +// orderId: null, +// orderStorage: | null>null, +// }, +// stateRecord: { +// $all: { +// on: { +// SHOW_TRACKING: '$self', +// CONNECTED: '$self', +// CONTEXT_LOADED: '$self', +// }, +// }, +// unresolved: { +// on: { +// IMPORT: 'resolving', +// }, +// }, +// resolving: { +// on: { +// CONNECTED: 'loading', +// }, +// }, +// loading: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// tracking: { +// on: { +// REQUEST_UPDATE: 'reloading', +// INVALID_ORDER: 'notFound', +// }, +// }, +// reloading: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// notFound: { +// on: { +// CONTEXT_LOADED: 'tracking', +// }, +// }, +// }, +// }); -pageOrderTrackingFsm.signal.subscribe(async (state) => { - // logger.logMethodArgs('pageOrderTrackingFsm.changed', state); - switch (state.by) { - case 'IMPORT': { - // just in unresolved - topAppBarContextProvider.setValue({ - headlineKey: 'loading', - }); - if (orderStorageContextConsumer.getValue() == null) { - fetchOrderStorage(); - } - break; - } +// pageOrderTrackingFsm.signal.subscribe(async (state) => { +// // logger.logMethodArgs('pageOrderTrackingFsm.changed', state); +// switch (state.by) { +// case 'IMPORT': { +// // just in unresolved +// topAppBarContextProvider.setValue({ +// headlineKey: 'loading', +// }); +// if (orderStorageContextConsumer.getValue() == null) { +// fetchOrderStorage(); +// } +// break; +// } - case 'CONNECTED': { - topAppBarContextProvider.setValue({ - headlineKey: 'page_order_tracking_headline', - }); - break; - } +// case 'CONNECTED': { +// topAppBarContextProvider.setValue({ +// headlineKey: 'page_order_tracking_headline', +// }); +// break; +// } - case 'REQUEST_UPDATE': { - await fetchOrderStorage(); // if not changed signal not fired! - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); - break; - } - } +// case 'REQUEST_UPDATE': { +// await fetchOrderStorage(); // if not changed signal not fired! +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// break; +// } +// } - if (state.to === 'loading') { - if (pageOrderTrackingFsm.context.orderStorage != null) { - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); - } - } -}); +// if (state.target === 'loading') { +// if (pageOrderTrackingFsm.context.orderStorage != null) { +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// } +// } +// }); -orderStorageContextConsumer.subscribe((orderStorage) => { - pageOrderTrackingFsm.context.orderStorage = orderStorage; - pageOrderTrackingFsm.transition('CONTEXT_LOADED'); -}); +// orderStorageContextConsumer.subscribe((orderStorage) => { +// pageOrderTrackingFsm.context.orderStorage = orderStorage; +// pageOrderTrackingFsm.transition('CONTEXT_LOADED'); +// }); -eventListener.subscribe('page_order_tracking_reload_click_event', () => { - pageOrderTrackingFsm.transition('REQUEST_UPDATE'); -}); +// eventListener.subscribe('page_order_tracking_reload_click_event', () => { +// pageOrderTrackingFsm.transition('REQUEST_UPDATE'); +// }); diff --git a/uniquely/com-pwa/src/manager/index.ts b/uniquely/com-pwa/src/manager/index.ts index 11e6b976b..8d49127f5 100644 --- a/uniquely/com-pwa/src/manager/index.ts +++ b/uniquely/com-pwa/src/manager/index.ts @@ -1,5 +1,7 @@ import './context-provider/home-page-content.js'; import './context-provider/l18e.js'; import './context-provider/order-storage.js'; +import './context-provider/product-price-storage.js'; +import './context-provider/product-storage.js'; import './context-provider/user.js'; import './submit-order-command-handler.js'; diff --git a/uniquely/com-pwa/src/manager/submit-order-command-handler.ts b/uniquely/com-pwa/src/manager/submit-order-command-handler.ts index 5947f35ff..de0cb8351 100644 --- a/uniquely/com-pwa/src/manager/submit-order-command-handler.ts +++ b/uniquely/com-pwa/src/manager/submit-order-command-handler.ts @@ -1,12 +1,20 @@ import {serviceRequest} from '@alwatr/fetch'; -import {commandHandler, contextProvider} from '@alwatr/signal'; +import {commandHandler, contextConsumer, contextProvider, requestableContextConsumer} from '@alwatr/signal'; -import {userContextConsumer, submitOrderCommandTrigger, orderStorageContextConsumer} from './context.js'; + +import {submitOrderCommandTrigger} from './context.js'; import {logger} from './logger.js'; import {config} from '../config.js'; +import type {AlwatrDocumentStorage, User} from '@alwatr/type'; import type {Order} from '@alwatr/type/customer-order-management.js'; + +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const userContextConsumer = contextConsumer.bind('user-context'); + commandHandler.define(submitOrderCommandTrigger.id, async (order) => { const userContext = userContextConsumer.getValue() ?? await userContextConsumer.untilChange(); @@ -24,7 +32,7 @@ commandHandler.define(submitOrderCommandTrigger.id, async ( const newOrder = response.data; - const orderStorage = orderStorageContextConsumer.getValue(); + const orderStorage = orderStorageContextConsumer.getValue().content; if (orderStorage != null) { orderStorage.data[newOrder.id] = newOrder; orderStorage.meta.lastUpdated = Date.now(); diff --git a/uniquely/com-pwa/src/ui/alwatr-pwa.ts b/uniquely/com-pwa/src/ui/alwatr-pwa.ts index 7f6ae5a48..3c98eca58 100644 --- a/uniquely/com-pwa/src/ui/alwatr-pwa.ts +++ b/uniquely/com-pwa/src/ui/alwatr-pwa.ts @@ -7,10 +7,7 @@ import '@alwatr/ui-kit/style/theme/palette-270.css'; import './page/home.js'; // for perf import './stuff/app-footer.js'; -import {pageNewOrderStateMachine} from '../manager/controller/new-order.js'; -import {pageOrderDetailStateMachine} from '../manager/controller/order-detail.js'; -import {pageOrderListStateMachine} from '../manager/controller/order-list.js'; -import {pageOrderTrackingFsm} from '../manager/controller/order-tracking.js'; +import {topAppBarContextProvider} from '../manager/context.js'; import type {RoutesConfig} from '@alwatr/router'; @@ -37,33 +34,27 @@ class AlwatrPwa extends AlwatrPwaElement { return html`...`; }, 'order-list': () => { - if (pageOrderListStateMachine.state.to === 'unresolved') { - pageOrderListStateMachine.transition('IMPORT'); - import('./page/order-list.js'); - } + topAppBarContextProvider.setValue({headlineKey: 'loading'}); + import('./page/order-list.js'); return html`...`; }, 'order-detail': (routeContext) => { - if (pageOrderDetailStateMachine.state.to === 'unresolved') { - pageOrderDetailStateMachine.transition('IMPORT'); - import('./page/order-detail.js'); - } - pageOrderDetailStateMachine.transition('SHOW_DETAIL', {orderId: +routeContext.sectionList[1]}); - return html`...`; + topAppBarContextProvider.setValue({headlineKey: 'loading'}); + import('./page/order-detail.js'); + return html`...`; }, 'order-tracking': (routeContext) => { - if (pageOrderTrackingFsm.state.to === 'unresolved') { - pageOrderTrackingFsm.transition('IMPORT'); - import('./page/order-tracking.js'); - } - pageOrderTrackingFsm.transition('SHOW_TRACKING', {orderId: +routeContext.sectionList[1]}); - return html`...`; + topAppBarContextProvider.setValue({headlineKey: 'loading'}); + import('./page/order-tracking.js'); + return html`...`; }, 'new-order': () => { - if (pageNewOrderStateMachine.state.to === 'unresolved') { - pageNewOrderStateMachine.transition('IMPORT'); - import('./page/new-order.js'); - } + topAppBarContextProvider.setValue({headlineKey: 'loading'}); + import('./page/new-order.js'); return html`...`; }, }, diff --git a/uniquely/com-pwa/src/ui/page/new-order.ts b/uniquely/com-pwa/src/ui/page/new-order.ts index 390158d37..99d652271 100644 --- a/uniquely/com-pwa/src/ui/page/new-order.ts +++ b/uniquely/com-pwa/src/ui/page/new-order.ts @@ -1,179 +1,626 @@ -import {customElement, html, StateMachineMixin, UnresolvedMixin} from '@alwatr/element'; +import {customElement, FiniteStateMachineController, html, state, UnresolvedMixin} from '@alwatr/element'; import {message} from '@alwatr/i18n'; +import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; +import {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; +import {tileQtyStep, orderInfoSchema} from '@alwatr/type/customer-order-management.js'; +import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; +import {getLocalStorageItem} from '@alwatr/util'; +import {validator} from '@alwatr/validator'; -import {buttons, pageNewOrderStateMachine} from '../../manager/controller/new-order.js'; +import {scrollToTopCommand, submitOrderCommandTrigger, topAppBarContextProvider} from '../../manager/context.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; import '../stuff/select-product.js'; +import type { + Order, + OrderDraft, + OrderShippingInfo, + OrderItem, + Product, + ProductPrice, +} from '@alwatr/type/customer-order-management.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-new-order': AlwatrPageNewOrder; } } +const productStorageContextConsumer = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); + +const productPriceStorageContextProvider = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('product-price-context'); + +const finalProductPriceStorageContextProvider = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productPriceStorageName: string} +>('final-product-price-context'); + +const newOrderLocalStorageKey = 'draft-order-x2'; + +const buttons = { + back: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'page_new_order_back_click_event', + }, + backToHome: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'back_to_home_click_event', + }, + editItems: { + icon: 'create-outline', + clickSignalId: 'page_new_order_edit_items_click_event', + }, + submit: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_click_event', + }, + edit: { + icon: 'create-outline', + clickSignalId: 'page_new_order_edit_click_event', + }, + submitFinal: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_final_click_event', + }, + submitShippingForm: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_submit_shipping_form_click_event', + }, + editShippingForm: { + icon: 'checkmark-outline', + clickSignalId: 'page_new_order_edit_shipping_form_click_event', + }, + newOrder: { + icon: 'add-outline', + clickSignalId: 'page_new_order_new_order_click_event', + }, + detail: { + icon: 'information-outline', + clickSignalId: 'page_new_order_detail_click_event', + }, + tracking: { + icon: 'chatbox-outline', + clickSignalId: 'page_new_order_tracking_click_event', + }, + reload: { + icon: 'reload-outline', + // flipRtl: true, + clickSignalId: 'order_list_reload_click_event', + }, + retry: { + icon: 'reload-outline', + clickSignalId: 'page_new_order_retry_click_event', + }, +} as const; + /** * Alwatr Customer Order Management Order Form Page */ @customElement('alwatr-page-new-order') -export class AlwatrPageNewOrder extends StateMachineMixin( - pageNewOrderStateMachine, - UnresolvedMixin(AlwatrOrderDetailBase), -) { - protected override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); - } +export class AlwatrPageNewOrder extends UnresolvedMixin(AlwatrOrderDetailBase) { + private _stateMachine = new FiniteStateMachineController(this, { + id: 'new_order_' + this.ali, + initial: 'pending', + context: { + registeredOrderId: null, + order: getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}), + productStorage: | null>null, + priceStorage: | null>null, + finalPriceStorage: | null>null, + }, + stateRecord: { + $all: { + entry: (): void => { + this.gotState = this._stateMachine.state.target; + localStorage.setItem(newOrderLocalStorageKey, JSON.stringify(this._stateMachine.context.order)); + }, + on: {}, + }, + pending: { + entry: (): void => { + const productStorage = productStorageContextConsumer.getValue(); + const priceStorage = productPriceStorageContextProvider.getValue(); + const finalPriceStorage = finalProductPriceStorageContextProvider.getValue(); + if (productStorage.state == 'initial') productStorageContextConsumer.request({productStorageName: 'tile'}); + if (priceStorage.state == 'initial') { + productPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + } + if (finalPriceStorage.state == 'initial') { + finalProductPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + } + }, + on: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { + target: 'edit', + condition: (): boolean => { + if ( + productStorageContextConsumer.getValue().state === 'complete' && + productPriceStorageContextProvider.getValue().state === 'complete' && + finalProductPriceStorageContextProvider.getValue().state === 'complete' + ) { + return true; + } + return false; + }, + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + contextError: { + on: { + request_context: { + target: 'pending', + actions: (): void => { + productStorageContextConsumer.request({productStorageName: 'tile'}); + productPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + finalProductPriceStorageContextProvider.request({productPriceStorageName: 'tile'}); + }, + }, + }, + }, + edit: { + entry: (): void => { + if (this._stateMachine.state.from != 'selectProduct' && !this._stateMachine.context.order?.itemList?.length) { + this._stateMachine.transition('select_product'); + } + }, + on: { + select_product: { + target: 'selectProduct', + }, + edit_shipping: { + target: 'shippingForm', + actions: (): void => { + this._stateMachine.context.order.shippingInfo ??= {}; + }, + }, + submit: { + target: 'review', + condition: (): boolean => { + if ( + !this._stateMachine.context.order.itemList?.length && + this._stateMachine.context.order.shippingInfo == null + ) { + return false; + } + // else + return this.validateOrder(); + }, + }, + qty_update: {}, + }, + }, + selectProduct: { + entry: (): void => { + if (this._stateMachine.state.target != this._stateMachine.state.from) { + scrollToTopCommand.request({}); + } + }, + on: { + submit: { + target: 'edit', + }, + }, + }, + reloading: { + on: { + context_request_error: { + target: 'edit', + actions: (): void => snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + context_request_complete: { + target: 'edit', + }, + }, + }, + shippingForm: { + on: { + submit: { + target: 'edit', + }, + }, + }, + review: { + on: { + back: {}, + final_submit: { + target: 'submitting', + actions: async (): Promise => { + const order = await submitOrderCommandTrigger.requestWithResponse(this._stateMachine.context.order); + if (order == null) { + this._stateMachine.transition('submit_failed'); + return; + } + // else + this._stateMachine.transition('submit_success', {registeredOrderId: order.id}); + }, + }, + }, + }, + submitting: { + on: { + submit_success: { + target: 'submitSuccess', + actions: (): void => { + localStorage.removeItem(newOrderLocalStorageKey); + this._stateMachine.context.order = + getLocalStorageItem(newOrderLocalStorageKey, {id: 'new', status: 'draft'}); + }, + }, + submit_failed: { + target: 'submitFailed', + }, + }, + }, + submitSuccess: { + on: { + new_order: { + target: 'edit', + actions: (): void => { + this._stateMachine.context.registeredOrderId = null; + }, + }, + }, + }, + submitFailed: { + on: { + final_submit: { + target: 'submitting', + }, + }, + }, + }, + signalList: [ + { + signalId: buttons.submit.clickSignalId, + transition: 'submit', + }, + { + signalId: buttons.submitShippingForm.clickSignalId, + transition: 'submit', + }, + { + signalId: buttons.edit.clickSignalId, + transition: 'back', + }, + { + signalId: buttons.submitFinal.clickSignalId, + transition: 'final_submit', + }, + { + signalId: buttons.editItems.clickSignalId, + transition: 'final_submit', + }, + { + signalId: buttons.retry.clickSignalId, + transition: 'final_submit', + }, + { + signalId: buttons.editShippingForm.clickSignalId, + transition: 'edit_shipping', + }, + { + signalId: buttons.tracking.clickSignalId, + actions: (): void => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('new_order'); + redirect({sectionList: ['order-tracking', orderId]}); + }, + }, + { + signalId: buttons.detail.clickSignalId, + actions: (): void => { + const orderId = this._stateMachine.context.registeredOrderId as string; + this._stateMachine.transition('new_order'); + redirect({sectionList: ['order-detail', orderId]}); + }, + }, + { + signalId: buttons.newOrder.clickSignalId, + actions: (): void => { + this._stateMachine.transition('new_order'); + redirect('/new-order/'); + }, + }, + { + signalId: 'order_item_qty_add', + actions: (event: ClickSignalType): void => { + this.qtyUpdate(event.detail, 1); // TODO: set type with action + }, + }, + { + signalId: 'order_item_qty_remove', + actions: (event: ClickSignalType): void => { + this.qtyUpdate(event.detail, -1); + }, + }, + ], + }); - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'cloud-download-outline'); - } + @state() + gotState = this._stateMachine.state.target; - protected render_state_edit(): unknown { - this._logger.logMethod('render_state_detail'); - const order = this.stateMachine.context.order; - return [ - // this.render_part_status(order), - this.render_part_item_list(order.itemList ?? [], this.stateMachine.context.productStorage, true), - this.render_part_btn_product(), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_btn_shipping_edit(), - this.render_part_summary(order), - this.render_part_btn_submit(), - ]; - } + override connectedCallback(): void { + super.connectedCallback(); - protected render_state_review(): unknown { - this._logger.logMethod('render_state_review'); - const order = this.stateMachine.context.order; - if (!(order.itemList?.length && order.shippingInfo)) { - this.stateMachine.transition('BACK'); - return; - } - // else - return [ - this.render_part_status(order), - this.render_part_item_list(order.itemList, this.stateMachine.context.productStorage), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_summary(order), - this.render_part_btn_final_submit(), - ]; - } + this._signalListenerList.push( + productStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); - protected render_state_selectProduct(): unknown { - this._logger.logMethod('render_state_selectProduct'); - return html``; - } + this._signalListenerList.push( + productPriceStorageContextProvider.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {priceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); - protected render_state_shippingForm(): unknown { - this._logger.logMethod('render_state_shippingForm'); - const order = this.stateMachine.context.order; - order.shippingInfo ??= {}; - return [ - this.render_part_item_list(order.itemList ?? [], this.stateMachine.context.productStorage, false), - this.render_part_shipping_form(order.shippingInfo), - this.render_part_btn_shipping_submit(), - ]; - } + this._signalListenerList.push( + finalProductPriceStorageContextProvider.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {finalPriceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); - protected render_state_submitting(): unknown { - this._logger.logMethod('render_state_submitting'); - return this.render_part_message('page_new_order_submitting_message', 'cloud-upload-outline'); + this._signalListenerList.push( + finalProductPriceStorageContextProvider.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {finalPriceStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); } - protected render_state_submitSuccess(): unknown { - this._logger.logMethod('render_state_submitSuccess'); - return [ - this.render_part_message('page_new_order_submit_success_message', 'cloud-done-outline'), - this.render_part_btn_submit_success(), - ]; - } + protected override render(): unknown { + this._logger.logMethod('render'); + return this._stateMachine.render({ + pending: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToHome, + }); + const content: IconBoxContent = { + tinted: 1, + icon: 'cloud-download-outline', + headline: message('loading'), + }; + return html``; + }, - protected render_state_submitFailed(): unknown { - this._logger.logMethod('render_state_submitFailed'); - return [ - this.render_part_message('page_new_order_submit_failed_message', 'cloud-offline-outline'), - this.render_part_btn_submit_failed(), - ]; - } + edit: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_new_order_headline', + startIcon: buttons.backToHome, + }); + const order = this._stateMachine.context.order; + return [ + this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, true), + html` +
    + + ${message('page_new_order_edit_items')} + +
    + `, + this.render_part_shipping_info(order.shippingInfo), + html` +
    + + ${message('page_new_order_shipping_edit')} + +
    + `, + this.render_part_summary(order), + html` +
    + ${message('page_new_order_submit')} + +
    + `, + ]; + }, - protected render_part_btn_product(): unknown { - return html`
    - ${message('page_new_order_edit_items')} -
    `; - } + contextError: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, - protected render_part_btn_shipping_edit(): unknown { - return html`
    - ${message('page_new_order_shipping_edit')} -
    `; - } + reloading: 'selectProduct', + selectProduct: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_new_order_headline', + startIcon: buttons.backToHome, + }); + return [ + html``, + html` +
    + ${message('select_product_submit_button')} + +
    + `, + ]; + }, - protected render_part_btn_shipping_submit(): unknown { - return html`
    - ${message('page_new_order_shipping_submit')} -
    `; - } + shippingForm: () => { + const order = this._stateMachine.context.order; + return [ + this.render_part_item_list(order.itemList ?? [], this._stateMachine.context.productStorage, false), + this.render_part_shipping_form(order.shippingInfo as Partial), + html` +
    + + ${message('page_new_order_shipping_submit')} + +
    + `, + ]; + }, + + review: () => { + const order = this._stateMachine.context.order as Order; + return [ + this.render_part_status(order), + this.render_part_item_list(order.itemList, this._stateMachine.context.productStorage), + this.render_part_shipping_info(order.shippingInfo), + this.render_part_summary(order), + html` +
    + + ${message('page_new_order_edit')} + + + ${message('page_new_order_submit_final')} + +
    + `, + ]; + }, + + submitting: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submitting_message'), + icon: 'cloud-upload-outline', + tinted: 1, + }; + return html``; + }, + + submitSuccess: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submit_success_message'), + icon: 'cloud-done-outline', + tinted: 1, + }; + return [ + html``, + html` +
    + + ${message('page_new_order_detail_button')} + + + ${message('page_new_order_headline')} + +
    + `, + ]; + }, - protected render_part_btn_submit(): unknown { - return html` -
    - ${message('page_new_order_submit')} -
    - `; + submitFailed: () => { + const content: IconBoxContent = { + headline: message('page_new_order_submit_failed_message'), + icon: 'cloud-offline-outline', + tinted: 1, + }; + return [ + html``, + html` +
    + + ${message('page_new_order_retry_button')} + +
    + `, + ]; + }, + }); } - protected render_part_btn_submit_success(): unknown { - return html` -
    - ${message('page_new_order_detail_button')} - ${message('page_new_order_headline')} -
    - `; + protected calculateOrderPrice(): void { + const order = this._stateMachine.context.order; + let totalPrice = 0; + let finalTotalPrice = 0; + for (const item of order.itemList ?? []) { + totalPrice += item.price * item.qty * tileQtyStep; + finalTotalPrice += item.finalPrice * item.qty * tileQtyStep; + } + order.totalPrice = Math.round(totalPrice); + order.finalTotalPrice = Math.round(finalTotalPrice); } - protected render_part_btn_submit_failed(): unknown { - return html` -
    - ${message('page_new_order_retry_button')} -
    - `; + protected validateOrder(): boolean { + try { + validator(orderInfoSchema, this._stateMachine.context.order, true); + return true; + } + catch (err) { + const _err = err as Error & {cause?: Record}; + this._logger.incident('validateOrder', _err.name, 'validation failed', _err); + if (_err.cause?.itemPath?.indexOf('shippingInfo') !== -1) { + snackbarSignalTrigger.request({ + message: message('page_new_order_shipping_info_not_valid_message'), + }); + } + else { + snackbarSignalTrigger.request({ + message: message('page_new_order_order_not_valid_message'), + }); + } + return false; + } } - protected render_part_btn_final_submit(): unknown { - return html` -
    - ${message('page_new_order_edit')} - ${message('page_new_order_submit_final')} -
    - `; + protected qtyUpdate(orderItem: OrderItem, add: number): void { + const qty = orderItem.qty + add; + if (qty <= 0) return; + orderItem.qty = qty; + this._stateMachine.transition('qty_update'); } } diff --git a/uniquely/com-pwa/src/ui/page/order-detail.ts b/uniquely/com-pwa/src/ui/page/order-detail.ts index a7975d8c9..882a79ea8 100644 --- a/uniquely/com-pwa/src/ui/page/order-detail.ts +++ b/uniquely/com-pwa/src/ui/page/order-detail.ts @@ -1,61 +1,268 @@ -import { - customElement, - StateMachineMixin, - UnresolvedMixin, -} from '@alwatr/element'; +import {customElement, FiniteStateMachineController, html, property, state, UnresolvedMixin} from '@alwatr/element'; +import {message} from '@alwatr/i18n'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; +import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; -import {pageOrderDetailStateMachine} from '../../manager/controller/order-detail.js'; import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-order-detail': AlwatrPageOrderDetail; } } +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const productStorageContextConsumer = requestableContextConsumer.bind< + AlwatrDocumentStorage, + {productStorageName: string} +>('product-storage-context'); + +const buttons = { + backToOrderList: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'order_detail_back_to_order_list_event', + }, + reload: { + icon: 'reload-outline', + flipRtl: true, + clickSignalId: 'order_detail_reload_event', + }, +} as const; + /** - * Alwatr Customer Order Management Order Form Page + * Alwatr Customer Order Management Order Detail Page. */ @customElement('alwatr-page-order-detail') -export class AlwatrPageOrderDetail extends StateMachineMixin( - pageOrderDetailStateMachine, - UnresolvedMixin(AlwatrOrderDetailBase), -) { +export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase) { + private _stateMachine = new FiniteStateMachineController(this, { + id: 'order_detail_' + this.ali, + initial: 'pending', + context: { + orderId: null, + orderStorage: | null> null, + productStorage: | null> null, + }, + stateRecord: { + $all: { + entry: (): void => { + this.gotState = this._stateMachine.state.target; + }, + on: {}, + }, + pending: { + entry: (): void => { + const orderContext = orderStorageContextConsumer.getValue(); + const productContext = productStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') orderStorageContextConsumer.request(null); + if (productContext.state === 'initial') productStorageContextConsumer.request({productStorageName: 'tile'}); + }, + on: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { + target: 'detail', + condition: (): boolean => { + const orderStorage = orderStorageContextConsumer.getValue(); + const productStorage = productStorageContextConsumer.getValue(); + if (orderStorage.state !== 'complete' || productStorage.state !== 'complete') { + return false; + } + + if (this.orderId == null || orderStorage.content.data[this.orderId] == null) { + this._stateMachine.transition('not_found'); + return false; + } + else { + this._stateMachine.context.orderId = this.orderId; + } + + return true; + }, + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + contextError: { + on: { + request_context: { + target: 'pending', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request({productStorageName: 'tile'}); + }, + }, + }, + }, + detail: { + on: { + request_context: { + target: 'reloading', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request({productStorageName: 'tile'}); + }, + }, + not_found: { + target: 'notFound', + }, + }, + }, + notFound: { + on: {}, + }, + reloading: { + on: { + context_request_complete: { + target: 'detail', + condition: (): boolean => { + const orderStorage = orderStorageContextConsumer.getValue(); + const productStorage = productStorageContextConsumer.getValue(); + if (orderStorage.state !== 'complete' || productStorage.state !== 'complete') { + return false; + } + + if (this.orderId == null || orderStorage.content.data[this.orderId] == null) { + this._stateMachine.transition('not_found'); + return false; + } + else { + this._stateMachine.context.orderId = this.orderId; + } + + return true; + }, + }, + context_request_reloading: {}, + context_request_error: { + target: 'detail', + actions: (): void => snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + not_found: { + target: 'notFound', + }, + }, + }, + }, + signalList: [ + { + signalId: buttons.backToOrderList.clickSignalId, + actions: (): void => { + redirect({sectionList: ['order-list']}); + }, + }, + { + signalId: buttons.reload.clickSignalId, + transition: 'request_context', + }, + ], + }); + + @state() + gotState = this._stateMachine.state.target; + + @property({type: Number}) + orderId?: number; + + override connectedCallback(): void { + super.connectedCallback(); + + this._signalListenerList.push( + orderStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); + + this._signalListenerList.push( + productStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); + } + protected override render(): unknown { this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); - } - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'ellipsis-horizontal'); - } + return this._stateMachine.render({ + pending: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + headline: message('loading'), + icon: 'cloud-download-outline', + tinted: 1, + }; + return html``; + }, - protected render_state_notFound(): unknown { - this._logger.logMethod('render_state_notFound'); - return this.render_part_message('page_order_detail_not_found', 'close'); - } + contextError: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, - protected render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_detail(); - } + reloading: 'detail', + + detail: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_detail_headline', + startIcon: buttons.backToOrderList, + endIconList: [{...buttons.reload, disabled: this.gotState === 'reloading'}], + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; + return [ + this.render_part_status(order), + this.render_part_item_list(order.itemList, this._stateMachine.context.productStorage), + this.render_part_shipping_info(order.shippingInfo), + this.render_part_summary(order), + ]; + }, - protected render_state_detail(): unknown { - this._logger.logMethod('render_state_detail'); - - // validate order id - const order = this.stateMachine.context.orderStorage?.data[this.stateMachine.context.orderId ?? ''] ?? null; - if (order === null) { - this.stateMachine.transition('INVALID_ORDER'); - return; - } - - return [ - this.render_part_status(order), - this.render_part_item_list(order.itemList, this.stateMachine.context.productStorage), - this.render_part_shipping_info(order.shippingInfo), - this.render_part_summary(order), - ]; + notFound: () => { + const content: IconBoxContent = { + headline: message('page_order_detail_not_found'), + icon: 'close', + tinted: 1, + }; + return html``; + }, + }); } } diff --git a/uniquely/com-pwa/src/ui/page/order-list.ts b/uniquely/com-pwa/src/ui/page/order-list.ts index 6e7f13fc0..b6396c1e6 100644 --- a/uniquely/com-pwa/src/ui/page/order-list.ts +++ b/uniquely/com-pwa/src/ui/page/order-list.ts @@ -6,27 +6,57 @@ import { SignalMixin, AlwatrBaseElement, UnresolvedMixin, - StateMachineMixin, + FiniteStateMachineController, + state, + ScheduleUpdateToFrameMixin, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; +import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; +import {Order} from '@alwatr/type/customer-order-management.js'; import '@alwatr/ui-kit/button/button.js'; import {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; -import {pageOrderListStateMachine, buttons} from '../../manager/controller/order-list.js'; +import {topAppBarContextProvider} from '../../manager/context.js'; import '../stuff/order-list.js'; +import type {AlwatrDocumentStorage, ClickSignalType} from '@alwatr/type'; + declare global { interface HTMLElementTagNameMap { 'alwatr-page-order-list': AlwatrPageOrderList; } } +const buttons = { + backToHome: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'back_to_home_click_event', + }, + reload: { + icon: 'reload-outline', + // flipRtl: true, + clickSignalId: 'order_list_reload_click_event', + }, + newOrder: { + icon: 'add-outline', + clickSignalId: 'order_list_new_order_click_event', + }, + orderDetail: { + clickSignalId: 'order_list_order_detail_click_event', + }, +} as const; + +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + /** * List of all orders. */ @customElement('alwatr-page-order-list') -export class AlwatrPageOrderList extends StateMachineMixin( - pageOrderListStateMachine, +export class AlwatrPageOrderList extends ScheduleUpdateToFrameMixin( UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))), ) { static override styles = css` @@ -41,42 +71,163 @@ export class AlwatrPageOrderList extends StateMachineMixin( transform: opacity var(--sys-motion-duration-small); } - :host([state=reloading]) alwatr-order-list { + :host([state='reloading']) alwatr-order-list { opacity: var(--sys-surface-disabled-opacity); } `; - override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); - } + private _stateMachine = new FiniteStateMachineController(this, { + id: 'order_list_' + this.ali, + initial: 'pending', + context: { + orderStorage: | null>null, + }, + stateRecord: { + $all: { + entry: (): void => { + this.gotState = this._stateMachine.state.target; + }, + on: {}, + }, + pending: { + entry: (): void => { + const orderContext = orderStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') { + orderStorageContextConsumer.request(null); + } + }, + on: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { + target: 'list', + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + contextError: { + on: { + request_context: { + target: 'pending', + actions: (): void => orderStorageContextConsumer.request(null), + }, + }, + }, + list: { + on: { + request_context: { + target: 'reloading', + actions: (): void => orderStorageContextConsumer.request(null), + }, + }, + }, + reloading: { + on: { + context_request_error: { + target: 'list', + actions: (): void => + snackbarSignalTrigger.request({ + messageKey: 'fetch_failed_description', + }), + }, + context_request_complete: { + target: 'list', + }, + }, + }, + }, + signalList: [ + { + signalId: buttons.reload.clickSignalId, + transition: 'request_context', + }, + { + signalId: buttons.newOrder.clickSignalId, + actions: (): void => { + redirect({ + sectionList: ['new-order'], + }); + }, + }, + { + signalId: buttons.orderDetail.clickSignalId, + actions: (event: ClickSignalType): void => { + redirect({sectionList: ['order-detail', event.detail.id]}); + }, + }, + ], + }); - render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return this.render_part_message('loading', 'cloud-download-outline'); - } + @state() + gotState = this._stateMachine.state.target; - render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_list(); - } + override connectedCallback(): void { + super.connectedCallback(); - render_state_list(): unknown { - this._logger.logMethod('render_state_list'); - return html``; + this._signalListenerList.push( + orderStorageContextConsumer.subscribe( + (context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, + {receivePrevious: 'NextCycle'}, + ), + ); } - protected render_part_message(key: string, icon: string): unknown { - this._logger.logMethod('render_part_message'); - const content: IconBoxContent = { - headline: message(key), - icon: icon, - tinted: 1, - }; + override render(): unknown { + this._logger.logMethod('render'); + return this._stateMachine.render({ + pending: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'loading', + startIcon: buttons.backToHome, + }); + const content: IconBoxContent = { + tinted: 1, + icon: 'cloud-download-outline', + headline: message('loading'), + }; + return html``; + }, + + contextError: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, + + reloading: 'list', - return html``; + list: () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_list_headline', + startIcon: buttons.backToHome, + endIconList: [buttons.newOrder, {...buttons.reload, disabled: this.gotState === 'reloading'}], + }); + return html``; + }, + }); } } diff --git a/uniquely/com-pwa/src/ui/page/order-tracking.ts b/uniquely/com-pwa/src/ui/page/order-tracking.ts index c153ecd44..bae4217d8 100644 --- a/uniquely/com-pwa/src/ui/page/order-tracking.ts +++ b/uniquely/com-pwa/src/ui/page/order-tracking.ts @@ -1,89 +1,252 @@ import { customElement, - css, + FiniteStateMachineController, html, - StateMachineMixin, + property, + state, UnresolvedMixin, - LocalizeMixin, - SignalMixin, - AlwatrBaseElement, } from '@alwatr/element'; import {message} from '@alwatr/i18n'; -import '@alwatr/icon'; -import '@alwatr/ui-kit/button/button.js'; -import '@alwatr/ui-kit/card/icon-box.js'; -import '@alwatr/ui-kit/card/surface.js'; +import {topAppBarContextProvider} from '@alwatr/pwa-helper/context.js'; +import {redirect} from '@alwatr/router'; +import {requestableContextConsumer} from '@alwatr/signal'; import '@alwatr/ui-kit/chat/chat.js'; -import '@alwatr/ui-kit/radio-group/radio-group.js'; +import {snackbarSignalTrigger} from '@alwatr/ui-kit/snackbar/show-snackbar.js'; -import {pageOrderTrackingFsm} from '../../manager/controller/order-tracking.js'; -import '../stuff/order-status-box.js'; +import {AlwatrOrderDetailBase} from '../stuff/order-detail-base.js'; + +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {Order, Product} from '@alwatr/type/customer-order-management.js'; +import type {IconBoxContent} from '@alwatr/ui-kit/card/icon-box.js'; declare global { interface HTMLElementTagNameMap { - 'alwatr-page-order-tracking': AlwatrPageOrderTracking; + 'alwatr-page-order-tracking': AlwatrPageOrderDetail; } } +const orderStorageContextConsumer = + requestableContextConsumer.bind>('order-storage-context'); + +const productStorageContextConsumer = + requestableContextConsumer.bind>('product-storage-tile-context'); + +const buttons = { + backToOrderList: { + icon: 'arrow-back-outline', + flipRtl: true, + clickSignalId: 'order_tracking_back_to_order_list_event', + }, + reload: { + icon: 'reload-outline', + flipRtl: true, + clickSignalId: 'order_tracking_reload_event', + }, +} as const; + /** - * Alwatr Customer Order Management Order Form Page + * Alwatr Customer Order Management Order Tracking Page. */ @customElement('alwatr-page-order-tracking') -export class AlwatrPageOrderTracking extends StateMachineMixin( - pageOrderTrackingFsm, - UnresolvedMixin(LocalizeMixin(SignalMixin(AlwatrBaseElement))), -) { - static override styles = css` - :host { - display: flex; - flex-direction: column; - padding: calc(2 * var(--sys-spacing-track)); - box-sizing: border-box; - min-height: 100%; - } - - alwatr-order-status-box { - margin-bottom: var(--sys-spacing-track); - } - - alwatr-chat { - flex-grow: 1; - } - `; +export class AlwatrPageOrderDetail extends UnresolvedMixin(AlwatrOrderDetailBase) { + private _stateMachine = new FiniteStateMachineController(this, { + id: 'order_tracking_' + this.ali, + initial: 'pending', + context: { + orderId: null, + orderStorage: | null> null, + productStorage: | null> null, + }, + stateRecord: { + '$all': { + entry: (): void => { + this.gotState = this._stateMachine.state.target; + }, + on: {}, + }, + 'pending': { + entry: (): void => { + const orderContext = orderStorageContextConsumer.getValue(); + const productContext = productStorageContextConsumer.getValue(); + if (orderContext.state === 'initial') { + orderStorageContextConsumer.request(null); + } + if (productContext.state === 'initial') { + productStorageContextConsumer.request(null); + } + }, + on: { + context_request_initial: {}, + context_request_pending: {}, + context_request_error: { + target: 'contextError', + }, + context_request_complete: { + target: 'tracking', + condition: (): boolean => { + if (this.orderId == null) { + this._stateMachine.transition('not_found'); + return false; + } + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; + }, + }, + context_request_reloading: { + target: 'reloading', + }, + }, + }, + 'contextError': { + on: { + request_context: { + target: 'pending', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); + }, + }, + }, + }, + 'tracking': { + on: { + request_context: { + target: 'reloading', + actions: (): void => { + orderStorageContextConsumer.request(null); + productStorageContextConsumer.request(null); + }, + }, + not_found: { + target: 'notFound', + }, + }, + }, + 'notFound': { + on: {}, + }, + 'reloading': { + on: { + context_request_error: { + target: 'tracking', + actions: (): void => + snackbarSignalTrigger.request({messageKey: 'fetch_failed_description'}), + }, + context_request_complete: { + target: 'tracking', + condition: (): boolean => { + if (orderStorageContextConsumer.getValue().state === 'complete' && + productStorageContextConsumer.getValue().state === 'complete' + ) return true; + return false; + }, + }, + }, + }, + }, + signalList: [ + { + signalId: buttons.backToOrderList.clickSignalId, + actions: (): void => {redirect({sectionList: ['order-list']});}, + }, + { + signalId: buttons.reload.clickSignalId, + transition: + 'request_context', + }, + ]}); - protected override render(): unknown { - this._logger.logMethod('render'); - return this[`render_state_${this.stateMachine.state.to}`]?.(); - } + @state() + gotState = this._stateMachine.state.target; - protected render_state_loading(): unknown { - this._logger.logMethod('render_state_loading'); - return message('loading'); + @property({type: Number}) + get orderId(): number { + return this.orderId; } - - protected render_state_notFound(): unknown { - this._logger.logMethod('render_state_notFound'); - return message('page_order_tracking_not_found'); + set orderId(orderId: number) { + this._stateMachine.transition('context_request_complete', {orderId}); } - protected render_state_reloading(): unknown { - this._logger.logMethod('render_state_reloading'); - return this.render_state_tracking(); + override connectedCallback(): void { + super.connectedCallback(); + + this._signalListenerList.push( + orderStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {orderStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), + ); + + this._signalListenerList.push( + productStorageContextConsumer.subscribe((context) => { + this._stateMachine.transition(`context_request_${context.state}`, {productStorage: context.content}); + }, {receivePrevious: 'NextCycle'}), + ); } - protected render_state_tracking(): unknown { - this._logger.logMethod('render_state_tracking'); + protected override render(): unknown { + this._logger.logMethod('render'); + + return this._stateMachine.render({ + 'pending': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + headline: message('loading'), + icon: 'cloud-download-outline', + tinted: 1, + }; + return html``; + }, + + 'contextError': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [buttons.reload], + }); + const content: IconBoxContent = { + icon: 'cloud-offline-outline', + tinted: 1, + headline: message('fetch_failed_headline'), + description: message('fetch_failed_description'), + }; + return html` + + + ${message('retry')} + + `; + }, + + 'reloading': 'tracking', - // validate order id - const order = this.stateMachine.context.orderStorage?.data[this.stateMachine.context.orderId ?? ''] ?? null; - if (order === null) { - this.stateMachine.transition('INVALID_ORDER'); - return; - } + 'tracking': () => { + topAppBarContextProvider.setValue({ + headlineKey: 'page_order_tracking_headline', + startIcon: buttons.backToOrderList, + endIconList: [{...buttons.reload, disabled: this.gotState === 'reloading'}], + }); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const order = this._stateMachine.context.orderStorage!.data[this._stateMachine.context.orderId!]; + return [ + html``, + html``, + ]; + }, - return [ - html``, - html``, - ]; + 'notFound': () => { + const content: IconBoxContent = { + headline: message('page_order_tracking_not_found'), + icon: 'close', + tinted: 1, + }; + return html``; + }, + }); } } diff --git a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts index 84d5eff15..4adc148bd 100644 --- a/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts +++ b/uniquely/com-pwa/src/ui/stuff/order-detail-base.ts @@ -18,7 +18,6 @@ import '@alwatr/ui-kit/card/surface.js'; import './order-shipping-form.js'; import './order-status-box.js'; import {config} from '../../config.js'; -import {qtyUpdate} from '../../manager/controller/new-order.js'; import type {AlwatrDocumentStorage} from '@alwatr/type'; import type {Order, OrderShippingInfo, OrderDraft, OrderItem, Product} from '@alwatr/type/customer-order-management.js'; @@ -251,7 +250,7 @@ export class AlwatrOrderDetailBase extends LocalizeMixin(SignalMixin(AlwatrBaseE const target = event.target as AlwatrTextField; if (target == null) return; const qty = target.value && +target.value ? +target.value : 100; - qtyUpdate(orderItem, qty - orderItem.qty); + // qtyUpdate(orderItem, qty - orderItem.qty); // FIXME: target.value = qty + ''; this.requestUpdate(); }}> diff --git a/uniquely/com-pwa/src/ui/stuff/select-product.ts b/uniquely/com-pwa/src/ui/stuff/select-product.ts index 319fdb428..2070fea20 100644 --- a/uniquely/com-pwa/src/ui/stuff/select-product.ts +++ b/uniquely/com-pwa/src/ui/stuff/select-product.ts @@ -7,14 +7,16 @@ import { html, mapObject, UnresolvedMixin, + property, + nothing, } from '@alwatr/element'; -import {message} from '@alwatr/i18n'; import '@alwatr/ui-kit/button/button.js'; import '@alwatr/ui-kit/card/product-card.js'; import {config} from '../../config.js'; -import {pageNewOrderStateMachine, buttons} from '../../manager/controller/new-order.js'; +import type {AlwatrDocumentStorage} from '@alwatr/type'; +import type {OrderDraft, Product, ProductPrice} from '@alwatr/type/customer-order-management.js'; import type {AlwatrProductCard, ProductCartContent} from '@alwatr/ui-kit/card/product-card.js'; declare global { @@ -24,7 +26,7 @@ declare global { } /** - * Alwatr Select Product. + * Alwatr Select Product Element. */ @customElement('alwatr-select-product') export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMixin(AlwatrBaseElement))) { @@ -50,105 +52,89 @@ export class AlwatrSelectProduct extends LocalizeMixin(SignalMixin(UnresolvedMix } `; + @property() + protected order?: OrderDraft; + + @property() + protected productStorage?: AlwatrDocumentStorage; + + @property() + protected finalPriceStorage?: AlwatrDocumentStorage; + + @property() + protected priceStorage?: AlwatrDocumentStorage; + selectedRecord: Record = {}; - override render(): unknown { - this._logger.logMethod('render'); + override connectedCallback(): void { + super.connectedCallback(); this._updateSelectedRecord(); - return [ - this.render_part_product_list(), - this.render_part_submit(), - ]; } private _updateSelectedRecord(): void { this._logger.logMethod('_updateSelectedRecord'); - const order = pageNewOrderStateMachine.context.order; this.selectedRecord = {}; - if (!order?.itemList?.length) return; - for (const item of order.itemList) { + if (!this.order?.itemList?.length) return; + for (const item of this.order.itemList) { this.selectedRecord[item.productId] = true; } } - protected render_part_submit(): unknown { - return html` -
    - ${message('select_product_submit_button')} - `; - } - - protected render_part_product_list(): unknown { - this._logger.logMethod('render_part_product_list'); - - const {productStorage, priceStorage, finalPriceStorage} = pageNewOrderStateMachine.context; + override render(): unknown { + this._logger.logMethod('render'); - if (productStorage == null || priceStorage == null || finalPriceStorage == null) { - return this._logger.accident( - 'render_part_product_list', - 'contenx_not_valid', - 'Some context not valid', - pageNewOrderStateMachine.context, - ); + if (this.productStorage == null || this.priceStorage == null || this.finalPriceStorage == null) { + this._logger.accident('render_part_product_list', 'context_not_valid', 'Some context not valid', this.order); + return nothing; } - return mapObject(this, productStorage?.data, (product) => { - const content: ProductCartContent = { - id: product.id, - title: product.title.fa, - imagePath: config.cdn + product.image.id, - price: priceStorage?.data[product.id]?.price ?? 0, - finalPrice: finalPriceStorage?.data[product.id]?.price ?? 0, - }; - // this._logger.logProperty('selected', !!this.selectedRecord[product.id]); - return html``; - }); + return mapObject(this, this.productStorage?.data, this.render_part_product_card); + } + + protected render_part_product_card(product: Product): unknown { + if (this.productStorage == null || this.priceStorage == null || this.finalPriceStorage == null) return; + const content: ProductCartContent = { + id: product.id, + title: product.title.fa, + imagePath: config.cdn + product.image.id, + price: this.priceStorage?.data[product.id]?.price ?? 0, + finalPrice: this.finalPriceStorage?.data[product.id]?.price ?? 0, + }; + return html``; } private _selectedChanged(event: CustomEvent): void { + if (this.order == null || this.priceStorage == null || this.finalPriceStorage == null) return; const target = event.target; const productId = target?.content?.id; - const {order, priceStorage, finalPriceStorage} = pageNewOrderStateMachine.context; this._logger.logMethodArgs('_selectedChanged', {productId}); - if (target == null || productId == null || priceStorage == null || finalPriceStorage == null) { - return this._logger.accident( - 'render_part_product_list', - 'context_not_valid', - 'Some context not valid', - {productId, priceStorage, finalPriceStorage}, - ); - } + if (target == null || productId == null) return; - order.itemList ??= []; + this.order.itemList ??= []; if (target.selected === true) { - order.itemList.push({ + this.order.itemList.push({ productId, qty: 0, - price: priceStorage.data[productId].price, - finalPrice: finalPriceStorage.data[productId].price, + price: this.priceStorage.data[productId].price, + finalPrice: this.finalPriceStorage.data[productId].price, }); } else { - const itemIndex = order.itemList.findIndex((item) => item.productId === productId); + const itemIndex = this.order.itemList.findIndex((item) => item.productId === productId); if (itemIndex !== -1) { - order.itemList.splice(itemIndex, 1); + this.order.itemList.splice(itemIndex, 1); } } const submitButton = this.renderRoot.querySelector('alwatr-button'); - if (submitButton && order.itemList.length < 2) { - submitButton.toggleAttribute('disabled', !order.itemList.length); + if (submitButton && this.order.itemList.length < 2) { + submitButton.toggleAttribute('disabled', !this.order.itemList.length); } } }