O_O
All checks were successful
Docker Deploy / build-and-push (push) Successful in 4m2s

This commit is contained in:
2026-01-20 10:32:14 -07:00
parent b5ac2e0608
commit 82b45fdfe4
8 changed files with 718 additions and 1 deletions

View File

@@ -263,6 +263,13 @@ const isDraft = invoice.status === 'draft';
<!-- Add Item Form (Only if Draft) -->
{isDraft && (
<div class="flex justify-end mb-4">
<button onclick="document.getElementById('import_time_modal').showModal()" class="btn btn-sm btn-outline gap-2">
<Icon name="heroicons:clock" class="w-4 h-4" />
Import Time
</button>
</div>
<form method="POST" action={`/api/invoices/${invoice.id}/items/add`} class="bg-base-200/50 p-4 rounded-lg mb-8 border border-base-300/50">
<h4 class="text-sm font-bold mb-3">Add Item</h4>
<div class="grid grid-cols-1 sm:grid-cols-12 gap-4 items-end">
@@ -374,4 +381,39 @@ const isDraft = invoice.status === 'draft';
<button>close</button>
</form>
</dialog>
<!-- Import Time Modal -->
<dialog id="import_time_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Import Time Entries</h3>
<p class="py-4">Import billable time entries for this client.</p>
<form method="POST" action={`/api/invoices/${invoice.id}/import-time`}>
<div class="grid grid-cols-2 gap-4 mb-4">
<div class="form-control">
<label class="label" for="start-date">Start Date</label>
<input type="date" id="start-date" name="startDate" class="input input-bordered" required />
</div>
<div class="form-control">
<label class="label" for="end-date">End Date</label>
<input type="date" id="end-date" name="endDate" class="input input-bordered" required />
</div>
</div>
<div class="form-control mb-6">
<label class="label cursor-pointer justify-start gap-4">
<input type="checkbox" name="groupByDay" class="checkbox" />
<span class="label-text">Group entries by day</span>
</label>
</div>
<div class="modal-action">
<button type="button" class="btn" onclick="document.getElementById('import_time_modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Import</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</DashboardLayout>

View File

@@ -2,7 +2,7 @@
import DashboardLayout from '../../../layouts/DashboardLayout.astro';
import { Icon } from 'astro-icon/components';
import { db } from '../../../db';
import { categories, members, organizations } from '../../../db/schema';
import { categories, members, organizations, tags } from '../../../db/schema';
import { eq } from 'drizzle-orm';
const user = Astro.locals.user;
@@ -40,6 +40,11 @@ const allCategories = await db.select()
.where(eq(categories.organizationId, orgId))
.all();
const allTags = await db.select()
.from(tags)
.where(eq(tags.organizationId, orgId))
.all();
const url = new URL(Astro.request.url);
const successType = url.searchParams.get('success');
---
@@ -206,6 +211,43 @@ const successType = url.searchParams.get('success');
</div>
</div>
<div class="divider">Defaults</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label font-medium" for="default-tax-rate">
Default Tax Rate (%)
</label>
<input
type="number"
id="default-tax-rate"
name="defaultTaxRate"
step="0.01"
min="0"
max="100"
value={organization.defaultTaxRate || 0}
class="input input-bordered w-full"
/>
</div>
<div class="form-control">
<label class="label font-medium" for="default-currency">
Default Currency
</label>
<select
id="default-currency"
name="defaultCurrency"
class="select select-bordered w-full"
>
<option value="USD" selected={!organization.defaultCurrency || organization.defaultCurrency === 'USD'}>USD ($)</option>
<option value="EUR" selected={organization.defaultCurrency === 'EUR'}>EUR (€)</option>
<option value="GBP" selected={organization.defaultCurrency === 'GBP'}>GBP (£)</option>
<option value="CAD" selected={organization.defaultCurrency === 'CAD'}>CAD ($)</option>
<option value="AUD" selected={organization.defaultCurrency === 'AUD'}>AUD ($)</option>
</select>
</div>
</div>
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 mt-6">
<span class="text-xs text-base-content/60 text-center sm:text-left">
Address information appears on invoices and quotes
@@ -220,6 +262,159 @@ const successType = url.searchParams.get('success');
</div>
</div>
<!-- Tags Section -->
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title">
<Icon name="heroicons:tag" class="w-6 h-6" />
Tags & Rates
</h2>
{/* We'll use a simple form submission for now or client-side JS for better UX later */}
<button onclick="document.getElementById('new_tag_modal').showModal()" class="btn btn-primary btn-sm">
<Icon name="heroicons:plus" class="w-5 h-5" />
Add Tag
</button>
</div>
<p class="text-base-content/70 mb-4">
Tags can be used to categorize time entries. You can also associate an hourly rate with a tag for billing purposes.
</p>
{allTags.length === 0 ? (
<div class="alert alert-info">
<Icon name="heroicons:information-circle" class="w-6 h-6" />
<div>
<div class="font-bold">No tags yet</div>
<div class="text-sm">Create tags to add context and rates to your time entries.</div>
</div>
</div>
) : (
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Rate / Hr</th>
<th class="w-20"></th>
</tr>
</thead>
<tbody>
{allTags.map(tag => (
<tr>
<td>
<div class="flex items-center gap-2">
{tag.color && (
<div class="w-3 h-3 rounded-full" style={`background-color: ${tag.color}`}></div>
)}
<span class="font-medium">{tag.name}</span>
</div>
</td>
<td>
{tag.rate ? (
<span class="font-mono">{new Intl.NumberFormat('en-US', { style: 'currency', currency: organization.defaultCurrency || 'USD' }).format(tag.rate / 100)}</span>
) : (
<span class="text-base-content/40 text-xs italic">No rate</span>
)}
</td>
<td>
<div class="flex gap-2">
<button
onclick={`document.getElementById('edit_tag_modal_${tag.id}').showModal()`}
class="btn btn-ghost btn-xs btn-square"
>
<Icon name="heroicons:pencil" class="w-4 h-4" />
</button>
<form method="POST" action={`/api/tags/${tag.id}/delete`} onsubmit="return confirm('Are you sure you want to delete this tag?');">
<button class="btn btn-ghost btn-xs btn-square text-error">
<Icon name="heroicons:trash" class="w-4 h-4" />
</button>
</form>
</div>
{/* Edit Modal */}
<dialog id={`edit_tag_modal_${tag.id}`} class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">Edit Tag</h3>
<form method="POST" action={`/api/tags/${tag.id}/update`}>
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">Name</span>
</label>
<input type="text" name="name" value={tag.name} class="input input-bordered w-full" required />
</div>
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">Color</span>
</label>
<input type="color" name="color" value={tag.color || '#3b82f6'} class="input input-bordered w-full h-12 p-1" />
</div>
<div class="form-control w-full mb-6">
<label class="label">
<span class="label-text">Hourly Rate (cents)</span>
</label>
<input type="number" name="rate" value={tag.rate || 0} min="0" class="input input-bordered w-full" />
<label class="label">
<span class="label-text-alt text-base-content/60">Enter rate in cents (e.g. 5000 = $50.00)</span>
</label>
</div>
<div class="modal-action">
<button type="button" class="btn" onclick={`document.getElementById('edit_tag_modal_${tag.id}').close()`}>Cancel</button>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
<dialog id="new_tag_modal" class="modal">
<div class="modal-box">
<h3 class="font-bold text-lg">New Tag</h3>
<form method="POST" action="/api/tags/create">
<input type="hidden" name="organizationId" value={organization.id} />
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">Name</span>
</label>
<input type="text" name="name" class="input input-bordered w-full" required placeholder="e.g. Billable, Rush" />
</div>
<div class="form-control w-full mb-4">
<label class="label">
<span class="label-text">Color</span>
</label>
<input type="color" name="color" value="#3b82f6" class="input input-bordered w-full h-12 p-1" />
</div>
<div class="form-control w-full mb-6">
<label class="label">
<span class="label-text">Hourly Rate (cents)</span>
</label>
<input type="number" name="rate" value="0" min="0" class="input input-bordered w-full" />
<label class="label">
<span class="label-text-alt text-base-content/60">Enter rate in cents (e.g. 5000 = $50.00)</span>
</label>
</div>
<div class="modal-action">
<button type="button" class="btn" onclick="document.getElementById('new_tag_modal').close()">Cancel</button>
<button type="submit" class="btn btn-primary">Create Tag</button>
</div>
</form>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<!-- Categories Section -->
<div class="card bg-base-100 shadow-xl border border-base-200 mb-6">
<div class="card-body">