You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rappli/src/components/Settings/Positions.tsx

418 lines
16 KiB
TypeScript

import {
JSX,
Component,
For,
useContext,
startTransition,
Show,
splitProps,
} from "solid-js";
import { produce, unwrap } from "solid-js/store";
import { autoAnimate } from "~/directives/autoAnimate";
import { sortable } from "~/directives/sortable";
import {
Position,
POSITION_TYPE_AGILE,
POSITION_TYPE_QUANTITY,
StoreContext,
UiStoreContext,
} from "~/stores";
import AddIcon from "~icons/carbon/add-filled";
import DeleteIcon from "~icons/carbon/trash-can";
import DragVerticalIcon from "~icons/carbon/drag-vertical";
import PositionSettingsIcon from "~icons/carbon/settings-adjust";
import { Checkbox, NumberInput, TextArea, TextInput } from "../Form";
import { MarkdownHelpLabel } from "../Markdown";
import {
createOptionalNumberInputHandler,
createNativeInputValue,
resetInput,
} from "~/util";
export const PositionsSettings: Component = () => {
const [state, setState] = useContext(StoreContext)!;
const [uiState, setUiState] = useContext(UiStoreContext)!;
autoAnimate;
sortable;
const AddPositionButton: Component<{ idx?: number }> = (props) => (
<button
onClick={() => {
const id = Math.random();
setState(
"positions",
produce((positions: Position[]) => {
const newPosition: Position = {
id,
enabled: true,
type: state.defaultPositionType,
name: "",
quantity: 1,
};
if (props.idx != null) {
positions.splice(props.idx, 0, newPosition);
} else {
positions.push(newPosition);
}
})
);
setUiState("selectedPosition", id);
}}
class="btn btn-xs btn-circle btn-outline btn-accent"
>
<AddIcon />
</button>
);
return (
<>
<div
class="grid grid-cols-1 gap-4"
use:sortable={{
handle: ".handle",
dragClass: "sortable-drag-card",
ghostClass: "sortable-ghost-card",
filter: ".drag-disabled",
forceFallback: true,
onEnd: (evt) => {
const oldIndex = evt.oldIndex;
const newIndex = evt.newIndex;
if (oldIndex == null || newIndex == null) {
return;
}
setState(
"positions",
produce((positions: any[]) => {
let item = positions[oldIndex];
positions.splice(oldIndex, 1);
positions.splice(newIndex, 0, unwrap(item));
})
);
},
}}
>
<For each={state.positions}>
{(position, idx) => {
let nameInputRef: HTMLInputElement = undefined!;
let positionNumberInputRef: HTMLInputElement = undefined!;
let onEnterKeyClose: JSX.EventHandler<
HTMLInputElement,
KeyboardEvent
> = (e) => {
if (e.code !== "Enter") {
return;
}
setUiState("selectedPosition", undefined);
};
const AgileDropdown: Component<
{ selected: number } & JSX.HTMLAttributes<HTMLSelectElement>
> = (p) => {
const [props, rest] = splitProps(p, ["selected", "class"]);
return (
<select
class={
"flex-1 w-0 pl-1 pr-0 select indent-0 select-xs select-bordered rounded-none " +
props.class
}
{...rest}
>
<For each={[0, 1, 2, 3, 5, 8, 13]}>
{(v) => (
<option selected={v === props.selected}>{v}</option>
)}
</For>
</select>
);
};
let quantityInputEl: HTMLInputElement = undefined!;
const quantityValue = createNativeInputValue(
() => quantityInputEl,
() => position.quantity
);
return (
<div class="indicator w-full">
<div class="indicator-item indicator-middle indicator-end flex items-center">
<div
title="Drag here"
class="rounded-sm bg-white p-1 py-3 shadow-sm border -ml-2 text-sm transition-transform cursor-pointer handle"
>
<DragVerticalIcon />
</div>
</div>
<div
data-idx={idx()}
class="relative z-10 bg-white w-full mr-4 card rounded-lg card-bordered card-compact shadow-sm"
>
<div class="card-body !p-2">
<div class="card-actions items-center justify-between gap-4 flex-nowrap">
<div
classList={{
"flex gap-2 overflow-hidden": true,
"line-through": !position.enabled,
}}
>
<div
class="font-bold cursor-pointer"
onClick={async () => {
await startTransition(function () {
setUiState("selectedPosition", position.id);
});
if (positionNumberInputRef) {
positionNumberInputRef.focus();
}
}}
>
{position.number || idx() + 1}
</div>
<div
class="min-w-[50px] min-h-[1rem] cursor-pointer text-ellipsis overflow-hidden whitespace-nowrap"
onClick={async () => {
await startTransition(function () {
setUiState("selectedPosition", position.id);
});
if (nameInputRef) {
nameInputRef.focus();
}
}}
>
{position.name}
</div>
</div>
<div class="flex gap-3">
<AddPositionButton idx={idx()} />
<button
classList={{
"btn btn-xs btn-circle": true,
"btn-outline":
uiState.selectedPosition !== position.id,
"btn-active":
uiState.selectedPosition === position.id,
}}
onClick={() =>
setUiState(
"selectedPosition",
uiState.selectedPosition !== position.id
? position.id
: undefined
)
}
>
<PositionSettingsIcon />
</button>
<button
onClick={() => {
setState(
"positions",
produce((positions: Position[]) =>
positions.splice(idx(), 1)
)
);
}}
class="btn btn-xs btn-circle btn-outline btn-error"
>
<DeleteIcon />
</button>
</div>
</div>
<div class="grid grid-cols-2 gap-1">
<div class="shrink form-control">
<div use:autoAnimate class="input-group input-group-xs">
<select
class="select select-xs select-bordered border-r-0"
onChange={(e) =>
setState(
"positions",
idx(),
"type",
e.currentTarget.value as any
)
}
>
<For
each={[
[POSITION_TYPE_QUANTITY, "Menge"],
[POSITION_TYPE_AGILE, "Agile"],
]}
>
{([type, label]) => (
<option
value={type}
selected={position.type === type}
>
{label}
</option>
)}
</For>
</select>
<Show when={position.type === POSITION_TYPE_QUANTITY}>
<div class="flex-1">
<input
ref={quantityInputEl}
class="w-full input input-bordered input-xs"
value={quantityValue()}
placeholder="Menge"
required
name="Menge"
onInput={createOptionalNumberInputHandler(
(v) => {
v != null &&
setState(
"positions",
idx(),
"quantity",
v
);
}
)}
onBlur={resetInput(0)}
/>
</div>
</Show>
<Show when={position.type === POSITION_TYPE_AGILE}>
<AgileDropdown
title="Story Points Minimum"
class="border-r-0"
onChange={(e) =>
setState(
"positions",
idx(),
"agilePointsMin",
parseInt(e.currentTarget.value)
)
}
selected={position.agilePointsMin || 0}
/>
<AgileDropdown
title="Story Points Maximum"
onChange={(e) =>
setState(
"positions",
idx(),
"agilePointsMax",
parseInt(e.currentTarget.value)
)
}
selected={position.agilePointsMax || 0}
/>
</Show>
</div>
</div>
<NumberInput
size="xs"
value={position.itemPrice}
label="Einzelpreis"
placeholder={
state.defaultItemPrice
? state.defaultItemPrice + ""
: undefined
}
onInput={(v) =>
setState("positions", idx(), "itemPrice", v)
}
/>
<div
use:autoAnimate
class="col-span-2 grid grid-cols-2 gap-1"
>
<Show when={uiState.selectedPosition === position.id}>
<div class="col-span-2">
<TextInput
ref={nameInputRef}
value={position.name}
label="Name"
required
onKeyDown={onEnterKeyClose}
onInput={(e) => {
setState(
"positions",
idx(),
"name",
e.currentTarget.value
);
}}
/>
</div>
<div class="col-span-2">
<TextInput
ref={positionNumberInputRef}
value={position.number}
label="Position"
onKeyDown={onEnterKeyClose}
onInput={(e) => {
setState(
"positions",
idx(),
"number",
e.currentTarget.value
);
}}
/>
</div>
<div class="col-span-2">
<NumberInput
label="Aktionspreis"
suffix="CHF"
value={position.fixedDiscountPrice}
onInput={(v) =>
setState(
"positions",
idx(),
"fixedDiscountPrice",
v
)
}
/>
</div>
<div class="col-span-2">
<TextArea
label="Beschreibung"
labelSuffixJsx={<MarkdownHelpLabel />}
value={position.description}
onInput={(evt) =>
setState(
"positions",
idx(),
"description",
evt.currentTarget.value
)
}
/>
</div>
<div class="col-span-2">
<Checkbox
checked={position.enabled}
onChange={(e) =>
setState(
"positions",
idx(),
"enabled",
e.currentTarget.checked
)
}
>
Position ist aktiv
</Checkbox>
</div>
</Show>
</div>
</div>
</div>
</div>
</div>
);
}}
</For>
</div>
<div class={"p-3 flex justify-center drag-disabled"}>
<AddPositionButton />
</div>
</>
);
};