parent
9124e66e28
commit
d3364af395
@ -1,5 +1,9 @@
|
||||
/target
|
||||
/resources
|
||||
/.env
|
||||
/*.db
|
||||
/*.db-shm
|
||||
/*.db-wal
|
||||
|
||||
/node_modules
|
||||
/*.lockb
|
||||
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptLibraryMappings">
|
||||
<file url="file://$PROJECT_DIR$" libraries="{alpinejs}" />
|
||||
</component>
|
||||
</project>
|
||||
@ -0,0 +1,3 @@
|
||||
{
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
# inventory-app
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Rust & Cargo
|
||||
1. [bun.sh](https://bun.sh/)
|
||||
|
||||
## Setup
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@ -0,0 +1,42 @@
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=templates");
|
||||
println!("cargo:rerun-if-changed=assets");
|
||||
println!("cargo:rerun-if-changed=tailwind.config.js");
|
||||
|
||||
std::fs::remove_dir_all("resources").unwrap_or_default();
|
||||
|
||||
Command::new("bun")
|
||||
.args([
|
||||
"run",
|
||||
"tailwindcss",
|
||||
"-c",
|
||||
"tailwind.config.js",
|
||||
"-i",
|
||||
"assets/public/css/tailwind.css",
|
||||
"-o",
|
||||
"resources/main.css",
|
||||
"--minify",
|
||||
])
|
||||
.status()
|
||||
.expect("failed to run tailwindcss");
|
||||
|
||||
copy_files("assets/static");
|
||||
}
|
||||
|
||||
fn copy_files(dir: &str) {
|
||||
for entry in std::fs::read_dir(dir).expect("failed to read dir `public`") {
|
||||
let entry = entry.expect("failed to read entry");
|
||||
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_files(entry.path().to_str().unwrap());
|
||||
} else {
|
||||
let path = entry.path();
|
||||
let filename = path.file_name().unwrap().to_str().unwrap();
|
||||
let dest = format!("resources/{}", filename);
|
||||
|
||||
std::fs::copy(path, dest).expect("failed to copy file");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "inventory-app",
|
||||
"dependencies": {},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.11",
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@
|
||||
export const content = ["./templates/**/*.html"]
|
||||
export const theme = {
|
||||
fontFamily: {
|
||||
sans: ['Graphik', 'sans-serif'],
|
||||
serif: ['Merriweather', 'serif'],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
'space-cadet':'#1f2041ff',
|
||||
'english-violet':'#4b3f72ff',
|
||||
'sunglow':'#ffc857ff',
|
||||
'dark-cyan':'#119da4ff',
|
||||
'paynes-gray':'#19647eff',
|
||||
'cerise': '#da4167ff',
|
||||
'orchid-pink': '#f0bcd4ff'
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@ -1,11 +1,14 @@
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
<div class="mx-auto ">
|
||||
<div class="flex flex-col">
|
||||
{% for item in items %}
|
||||
<li id="item-{{item.id}}-entry" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-6"><a href="/item/{{item.id}}/" hx-push-url="true">{{ item.name }}</a></div>
|
||||
<div class="mb-4 mx-1 flex flex-row">
|
||||
<div class="basis-1/2">
|
||||
<a class="font-medium text-paynes-gray dark:text-paynes-gray hover:underline"
|
||||
href="/item/{{item.id}}/" hx-push-url="true">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
|
||||
{% if items.len() > 0 %}
|
||||
<div class="card">
|
||||
<ul class="list-group list-group-flush">
|
||||
{% for item in items %}
|
||||
<li id="item-{{item.id}}-entry" class="list-group-item">
|
||||
<div class="row">
|
||||
<div class="col-6"><a href="/item/{{item.id}}/" hx-push-url="true">{{ item.name }}</a></div>
|
||||
<div class="col">Count: <span id="item-{{item.id}}-count" hx-get="/item/{{item.id}}/count" hx-trigger="load">0</span></div>
|
||||
<div class="col">Reorder Point: {{ item.reorder_point }}</div>
|
||||
<div class="mx-auto ">
|
||||
<div class="flex flex-col">
|
||||
{% for item in items %}
|
||||
<div class="mb-4 mx-1 flex flex-row">
|
||||
<div class="basis-1/2">
|
||||
<a class="font-medium text-paynes-gray dark:text-paynes-gray hover:underline"
|
||||
href="/item/{{item.id}}/" hx-push-url="true">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</div>
|
||||
<div class="basis-1/4">
|
||||
Count: <span id="item-{{item.id}}-count" hx-get="/item/{{item.id}}/count" hx-trigger="load">0</span>
|
||||
</div>
|
||||
<div class="basis-1/4">
|
||||
Reorder Point: {{ item.reorder_point }}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -1,65 +1,82 @@
|
||||
<form hx-post="/item/{{item_id}}/adjustment/negative"
|
||||
hx-target="this" hx-swap="outerHTML"
|
||||
x-ref="formNegativeAdjustment"
|
||||
x-data="{ reason: 'Sale' }">
|
||||
<input id="reason" name="reason" type="hidden" x-model="reason"/>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!--<label for="amount" class="form-label">Amount</label>-->
|
||||
<input type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="btn btn-primary" x-text="reason">Sale</button>
|
||||
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown"
|
||||
aria-expanded="false">
|
||||
<span class="visually-hidden">Other</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Sale'">
|
||||
Sale
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Destruction'">
|
||||
Destruction
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Expiration'">
|
||||
Expiration
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item"
|
||||
@click="reason = 'Theft'">
|
||||
Theft
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
x-data="{ reason: 'Sale', reason_dropdown_show: false }">
|
||||
<div class="mb-5">
|
||||
<label for="amount" class="block mb-2 text-sm font-medium">Amount</label>
|
||||
<input type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
class="border border-neutral-900 text-neutral-900 dark:text-slate-100 text-sm rounded-lg focus:ring-paynes-gray focus:border-paynes-gray block max-w-56 p-2.5"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="mt-2 text-sm text-cerise">
|
||||
{{ amount_error }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="mb-5 flex justify-start">
|
||||
<button
|
||||
class="text-slate-100 bg-english-violet hover:bg-dark-cyan focus:ring-4 focus:ring-dark-cyan font-medium rounded-l-lg text-sm px-5 py-2.5 focus:outline-none"
|
||||
x-text="reason">
|
||||
Sale
|
||||
</button>
|
||||
<div x-data="{ isOpen: false, openedWithKeyboard: false }"
|
||||
class="relative w-fit"
|
||||
x-on:keydown.esc.window="isOpen = false; openedWithKeyboard = false">
|
||||
<button type="button" x-on:click="isOpen = ! isOpen"
|
||||
class="text-slate-100 bg-english-violet hover:bg-dark-cyan focus:ring-4 focus:ring-dark-cyan font-medium rounded-r-lg text-sm px-3.5 py-3.5 focus:outline-none inline-flex items-center whitespace-nowrap rounded-radius border-l-2 border-slate-100 dark:border-neutral-900 tracking-wide transition"
|
||||
aria-haspopup="true" x-on:keydown.space.prevent="openedWithKeyboard = true"
|
||||
x-on:keydown.enter.prevent="openedWithKeyboard = true"
|
||||
x-on:keydown.down.prevent="openedWithKeyboard = true"
|
||||
x-bind:class="isOpen || openedWithKeyboard ? 'text-on-surface-strong dark:text-on-surface-dark-strong' : 'text-on-surface dark:text-on-surface-dark'"
|
||||
x-bind:aria-expanded="isOpen || openedWithKeyboard">
|
||||
<span class="sr-only">Reason Options</span>
|
||||
<svg aria-hidden="true" fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
stroke-width="2" stroke="currentColor" class="size-4 rotate-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Dropdown Menu -->
|
||||
<div x-cloak
|
||||
x-show="isOpen || openedWithKeyboard"
|
||||
x-transition
|
||||
x-trap="openedWithKeyboard"
|
||||
x-on:click.outside="isOpen = false, openedWithKeyboard = false"
|
||||
x-on:keydown.down.prevent="$focus.wrap().next()"
|
||||
x-on:keydown.up.prevent="$focus.wrap().previous()"
|
||||
class="bg-slate-100 dark:bg-neutral-900 absolute top-11 left-0 flex w-fit min-w-48 flex-col overflow-hidden rounded-radius border border-outline border-neutral-900 dark:border-slate-100"
|
||||
role="menu">
|
||||
<a class="px-4 py-2 text-sm hover:bg-dark-cyan hover:font-semibold focus-visible:bg-dark-cyan focus-visible:font-semibold focus-visible:outline-hidden"
|
||||
role="menuitem"
|
||||
@click="reason = 'Sale'; isOpen = false">
|
||||
Sale
|
||||
</a>
|
||||
<a class="px-4 py-2 text-sm hover:bg-dark-cyan hover:font-semibold focus-visible:bg-dark-cyan focus-visible:font-semibold focus-visible:outline-hidden"
|
||||
role="menuitem"
|
||||
@click="reason = 'Destruction'; isOpen = false">
|
||||
Destruction
|
||||
</a>
|
||||
<a class="px-4 py-2 text-sm hover:bg-dark-cyan hover:font-semibold focus-visible:bg-dark-cyan focus-visible:font-semibold focus-visible:outline-hidden"
|
||||
role="menuitem"
|
||||
@click="reason = 'Expiration'; isOpen = false">
|
||||
Expiration
|
||||
</a>
|
||||
<a class="px-4 py-2 text-sm hover:bg-dark-cyan hover:font-semibold focus-visible:bg-dark-cyan focus-visible:font-semibold focus-visible:outline-hidden"
|
||||
role="menuitem"
|
||||
@click="reason = 'Theft'; isOpen = false">
|
||||
Theft
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input id="reason" name="reason" type="hidden" x-model="reason"/>
|
||||
</form>
|
||||
|
||||
@ -1,43 +1,47 @@
|
||||
|
||||
<form hx-post="/item/{{item_id}}/adjustment/positive" hx-target="this" hx-swap="outerHTML" >
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<input type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
step="0.01"
|
||||
class="form-control"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="invalid-feedback">{{ amount_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col">
|
||||
<input type="text"
|
||||
id="price"
|
||||
name="price"
|
||||
placeholder="Price"
|
||||
class="form-control"
|
||||
aria-label="price"
|
||||
required
|
||||
{% if !price_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-price"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !price_error.is_empty() -%}
|
||||
<small id="invalid-price" class="invalid-feedback">{{ price_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="col">
|
||||
<button class="btn btn-primary">Add</button>
|
||||
</div>
|
||||
<form hx-post="/item/{{item_id}}/adjustment/positive"
|
||||
hx-target="this" hx-swap="outerHTML">
|
||||
<div class="mb-5">
|
||||
<label for="amount" class="block mb-2 text-sm font-medium">Amount</label>
|
||||
<input
|
||||
type="number"
|
||||
id="amount"
|
||||
name="amount"
|
||||
class="border border-neutral-900 text-neutral-900 dark:text-slate-100 text-sm rounded-lg focus:ring-paynes-gray focus:border-paynes-gray block max-w-56 p-2.5"
|
||||
step="0.01"
|
||||
placeholder="Amount"
|
||||
aria-label="amount"
|
||||
required
|
||||
{% if !amount_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-amount"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !amount_error.is_empty() -%}
|
||||
<small id="invalid-amount" class="mt-2 text-sm text-cerise">
|
||||
{{ amount_error }}
|
||||
</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="price" class="block mb-2 text-sm font-medium">Price</label>
|
||||
<input type="text"
|
||||
id="price"
|
||||
name="price"
|
||||
class="border border-neutral-900 text-neutral-900 dark:text-slate-100 text-sm rounded-lg focus:ring-paynes-gray focus:border-paynes-gray block max-w-56 p-2.5"
|
||||
placeholder="Price"
|
||||
aria-label="price"
|
||||
required
|
||||
{% if !price_error.is_empty() -%}
|
||||
aria-invalid="true"
|
||||
aria-describedby="invalid-price"
|
||||
{% endif -%}
|
||||
/>
|
||||
{% if !price_error.is_empty() -%}
|
||||
<small id="invalid-price" class="mt-2 text-sm text-cerise">{{ price_error }}</small>
|
||||
{% endif -%}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<button class="text-slate-100 bg-english-violet hover:bg-dark-cyan focus:ring-4 focus:ring-dark-cyan font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 focus:outline-none">Add</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,53 +1,60 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-bs-theme="dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<!-- <link rel="stylesheet" href="/css/pico.min.css" > -->
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<!--TODO Vendor css-->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<!--TODO Vendor this script -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
|
||||
<title>{% block title %}Title{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container mb-4">
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="/home">Inventory App</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse justify-content-end" id="navbarNav">
|
||||
<ul class="navbar-nav ">
|
||||
<li class="nav-item"><h1></h1></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/overview">Overview</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/catalog">Catalog</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/upload">Upload</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/reports">Reports</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/history">History</a></li>
|
||||
</ul>
|
||||
<div class="d-flex">
|
||||
<a class="btn btn-primary" href="/auth/logout">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
<body class="bg-slate-100 text-neutral-900 dark:bg-neutral-900 dark:text-slate-100 min-h-screen">
|
||||
<header class="bg-space-cadet font-sans mb-4 mx-auto">
|
||||
<nav class="mx-auto max-w-7xl px-2 py-4 sm:px-6 lg:px-8 flex justify-between gap-2">
|
||||
<div>
|
||||
<a href="/home" class="rounded-md bg-english-violet px-3 py-2 text-sm font-medium text-white">
|
||||
Inventory App
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/overview"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||
Overview
|
||||
</a>
|
||||
<a href="/catalog"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||
Catalog
|
||||
</a>
|
||||
<a href="/upload"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||
Upload
|
||||
</a>
|
||||
<a href="/reports"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||
Reports
|
||||
</a>
|
||||
<a href="/history"
|
||||
class="rounded-md px-3 py-2 text-sm font-medium text-gray-300 hover:bg-gray-700 hover:text-white">
|
||||
History
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main class="container">
|
||||
<main class="bg-slate-100 text-neutral-900 dark:bg-neutral-900 dark:text-slate-100 mx-auto px-1">
|
||||
{% block content %}<p>Content Missing</p>{% endblock %}
|
||||
</main>
|
||||
<footer class="container-fluid">
|
||||
<footer>
|
||||
<!--
|
||||
<script>
|
||||
// Needed for nice bootstrap dropdowns
|
||||
const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="dropdown"]');
|
||||
const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl))
|
||||
</script>
|
||||
-->
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in new issue