Files
Umbrella/frontend/src/routes/stock/Index.svelte
2025-11-26 08:26:42 +01:00

274 lines
8.6 KiB
Svelte

<script>
import { onMount } from 'svelte';
import { api, drop, get, patch } from '../../urls.svelte';
import { error, yikes } from '../../warn.svelte';
import { t } from '../../translations.svelte';
import ItemList from './ItemList.svelte';
import ItemProps from './ItemProps.svelte';
import LineEditor from '../../Components/LineEditor.svelte';
import Locations from './Locations.svelte';
import MarkdownEditor from '../../Components/MarkdownEditor.svelte';
import Notes from '../notes/RelatedNotes.svelte';
import Tags from '../tags/TagList.svelte';
let loc_data = $derived.by(loadLocation);
let item = $state(null);
let location = $state(null);
let draggedItem = $state(null)
let draggedLocation = $state(null)
let { item_id, location_id, owner, owner_id } = $props();
let skip_location = false; // disable effect on setting location within loadItem()
$effect(() => {
// This effect runs whenever `location` changes
if (!skip_location && location !== null) {
item = null;
setLocationUrl();
}
});
$effect(() => {
// This effect runs whenever `item` changes
if (item !== null) {
setItemUrl();
skip_location = false;
}
});
let properties = $state(null);
let top_level = $state(null);
async function deleteLocation(loc){
if (!confirm(t('confirm_delete',{element:loc.name}))) return;
const url = api(`stock/location/${loc.id}`);
const res = await drop(url);
if (res.ok){
yikes();
unlistLocation(loc);
} else error(res);
}
function drag_item(item){
draggedLocation = null;
draggedItem = item;
}
function drag_location(loc){
draggedItem = null;
draggedLocation = loc;
}
function dropNestedLocation(locations,loc){
for (let [idx,entry] of locations.entries()){
if (entry.id == loc.id){
locations.splice(idx,1);
return true;
}
if (entry.locations && dropNestedLocation(entry.locations,loc)) return true;
}
return false;
}
async function move_dragged_to(new_loc){
const data = draggedItem ? { item : draggedItem.id, target: new_loc.id } : { parent_location_id: new_loc.id }
const url = api(draggedItem ? 'stock/move_item' : `stock/location/${draggedLocation.id}`);
const res = await patch(url,data);
if (res.ok){
yikes();
location = new_loc;
if (!draggedItem) unlistLocation(draggedLocation);
draggedItem = null;
draggedLocation = null;
} else {
error(res);
}
}
async function loadLocation(){
if (!location) return null;
const url = api(`stock/location/${location.id}`);
const res = await get(url);
if (res.ok){
yikes();
return res.json();
} else {
error(res);
return null;
}
}
async function loadItem(){
if (!item_id) return;
const url = api(`stock/${owner}/${owner_id}/item/${item_id}`);
const res = await get(url);
if (res.ok){
yikes();
const json = await res.json();
const path = json.path;
for (let owner of top_level){
for (let loc of owner.locations){
if (loc.id == path.id) {
loc.locations = path.locations;
skip_location = true;
location = json.location;
break;
}
}
}
for (let i of json.items){
if (i.owner_number == +item_id) item = i;
}
} else {
error(res);
return null;
}
}
async function loadPath(){
if (!location_id) return;
const url = api(`stock/location/${location_id}`);
const res = await get(url);
if (res.ok){
yikes();
const json = await res.json();
const path = json.path;
for (let owner of top_level){
for (let loc of owner.locations){
if (loc.id == path.id) {
loc.locations = path.locations;
location = json.location;
break;
}
}
}
} else {
error(res);
return null;
}
}
async function loadProperties(){
const url = api('stock/properties')
const res = await get(url);
if (res.ok){
var json = await res.json();
var dict = {}
for (var entry of json.sort((a,b) => b.id - a.id)) dict[entry.name+'.'+entry.unit] = entry;
properties = null;
properties = Object.values(dict).sort((a,b) => a.name.localeCompare(b.name));
yikes();
} else error(res);
}
async function loadUserLocations(){
const url = api('stock/locations/of_user')
const res = await get(url);
if (res.ok){
top_level = await res.json();
yikes();
} else error(res);
}
async function load(){
await loadUserLocations();
await loadPath();
await loadProperties();
await loadItem();
}
function moveToTop(loc){
if (patchLocation(location,'parent_location_id',0)){
loc.parent_location_id = 0;
for (var owner of top_level){
if (owner.locations && dropNestedLocation(owner.locations,loc)) {
owner.locations.push(loc);
break;
};
}
}
}
async function patchLocation(location,field,newValue){
const data = {};
data[field] = newValue;
const url = api(`stock/location/${location.id}`);
const res = await patch(url,data);
if (res.ok){
yikes();
return true;
} else {
error(res);
return false;
}
}
function setItemUrl(){
var owner = `/${item.owner.type}/${item.owner.id}`
var code = `/item/${item.owner_number}`
let url = window.location.origin + '/stock' + owner + code;
window.history.replaceState(window.history.state, '', url);
}
function setLocationUrl(){
let url = window.location.origin + '/stock/location/' + location.id;
window.history.replaceState(window.history.state, '', url);
}
function unlistLocation(loc){
for (var owner of top_level){
if (owner.locations && dropNestedLocation(owner.locations,loc)) break;
}
}
onMount(load);
</script>
<h2>{t('Stock')}</h2>
<div class="grid3">
<div class="locations">
{#if top_level}
{#each top_level as realm,idx}
<h3>{realm.name}</h3>
{#if realm.locations}
<Locations
locations={realm.locations}
parent={realm.parent}
bind:selected={location}
{move_dragged_to}
drag_start={drag_location} />
{/if}
{/each}
{/if}
</div>
{#await loc_data}
<span>loading…</span>
{:then data}
<div class="items">
{#if location}
<h3>
<LineEditor editable={true} bind:value={location.name} type="span" onSet={newName => patchLocation(location,'name',newName)} />
<button class="symbol" title={t('delete_object',{object:t('location')})} onclick={e => deleteLocation(location)}></button>
{#if location.parent_location_id}
<button class="symbol" title={t('move_to_top')} onclick={e => moveToTop(location)}></button>
{/if}
</h3>
<MarkdownEditor editable={true} value={location.description} type="div" onSet={newDesc => patchLocation(location,'description',newDesc)} />
{/if}
<ItemList {location} items={data?.items.sort((a,b) => a.code.localeCompare(b.code))} bind:selected={item} drag_start={drag_item} />
</div>
<div class="properties">
<ItemProps {item} {properties} />
</div>
{#if item && data && data.users}
<div class="tags">
<span>{t('tags')}</span>
<Tags module="stock" id={item.id} user_list={data.users} />
</div>
<div class="notes">
<span>{t('notes')}</span>
<Notes module="stock" entity_id={item.id} />
</div>
{/if}
{/await}
</div>