274 lines
8.6 KiB
Svelte
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>
|