diff --git a/CLAUDE.md b/CLAUDE.md index bf966d12..3c4267b5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -100,4 +100,4 @@ Don't duplicate these here — they auto-load when you touch matching paths: ## Decision Log -When you make a non-obvious choice — picked approach A over B, declined a dependency, stopped because a rule here forbade something — append one line to `dev_docs/DECISIONS.md`: `[YYYY-MM-DD] — `. Check that file before re-litigating a past decision. +When you make a non-obvious choice — picked approach A over B, declined a dependency, stopped because a rule here forbade something — append one line to `DECISIONS.md` (repo root): `[YYYY-MM-DD] — `. Check that file before re-litigating a past decision. diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 00000000..7656a2b2 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,9 @@ +# Decision Log + +One line per decision: `[YYYY-MM-DD] — `. Appended by agents and humans when a non-obvious choice is made (approach picked over an alternative, dependency declined, action stopped by a CLAUDE.md rule). Read before re-litigating a past decision. + +[2026-07-02] Adopted this decision log — CLAUDE.md rewritten per config-over-prompt principles; decisions persist here instead of being re-derived each session. +[2026-07-03] Prod constraint clobber (self-inflicted, repaired in ~10 min): applied pending_operations link_document_to_voucher migration from a checkout predating 20260702171000 (retag_line_dimensions) — hand-copied CHECK lists clobber concurrent adds. Zero impact (no retag ops in window). Rule: before applying any expand-types migration to prod, diff the list against the LIVE prod constraint, not the local file history. Long-term fix queued in mcp_optimization_plan P0-1 follow-up (audit test now guards CI). +[2026-07-03] Archived 4 completed/superseded plans to dev_docs/archive/ (dimensions_implementation_plan, specialized-agent-plan, api_ai_architecture/PLAN, mcp-apps-architecture-reference) — moved, not deleted, because dev_docs is gitignored (no git history to recover from). Live remnants relocated first: PR10 backlog → dimensions_architecture.md; eval-harness spec → claude_surface_plan.md §2.1. agent_first_vision.md §8 marked superseded by claude_surface_plan.md (Skatteverket filing is BUILT, contra its P0 item 6). +[2026-07-03] Moved this log from dev_docs/DECISIONS.md to repo root — dev_docs/ is gitignored, so the log was invisible to other developers; root matches the existing convention (CONTRIBUTING.md, SECURITY.md). +[2026-07-03] Converted the last three full-page create flows (salary run, employee, recurring schedule) to ?new=1 URL-driven modals matching the verifikat/invoice pattern (#861); old /new routes survive as redirects for bookmarks/agent intents. Moved forms keep their existing hardcoded-Swedish strings — translating them is out of scope for the modal conversion. diff --git a/app/(dashboard)/invoices/recurring/new/page.tsx b/app/(dashboard)/invoices/recurring/new/page.tsx index 5893c07d..4565848c 100644 --- a/app/(dashboard)/invoices/recurring/new/page.tsx +++ b/app/(dashboard)/invoices/recurring/new/page.tsx @@ -1,385 +1,9 @@ -'use client' - -import { useState, useEffect, useMemo } from 'react' -import { useRouter } from 'next/navigation' -import Link from 'next/link' -import { useTranslations } from 'next-intl' -import { createClient } from '@/lib/supabase/client' -import { useForm, useFieldArray, Controller } from 'react-hook-form' -import { zodResolver } from '@hookform/resolvers/zod' -import { z } from 'zod' -import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { PageHeader } from '@/components/ui/page-header' -import { useToast } from '@/components/ui/use-toast' -import { useCompany } from '@/contexts/CompanyContext' -import { ArrowLeft, Plus, Trash2 } from 'lucide-react' -import type { Customer, Currency } from '@/types' -import { formatCurrency } from '@/lib/utils' - -const currencies: Currency[] = ['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK'] -const units = ['st', 'tim', 'dag', 'månad', 'km', 'kg'] +import { redirect } from 'next/navigation' +// Recurring-schedule creation now happens in a modal on the schedule list +// (matching the verifikat pattern) — the form itself lives in +// components/invoices/NewRecurringScheduleDialog.tsx. This route survives as +// a redirect so old links, bookmarks, and agent intents keep working. export default function NewRecurringSchedulePage() { - const router = useRouter() - const { toast } = useToast() - const { company } = useCompany() - const supabase = createClient() - const t = useTranslations('invoice_recurring_new') - const [customers, setCustomers] = useState([]) - const [isSubmitting, setIsSubmitting] = useState(false) - - const schema = useMemo(() => { - const itemSchema = z.object({ - description: z.string().min(1, t('validation_description_required')), - quantity: z.number().min(0.01, t('validation_quantity_min')), - unit: z.string().min(1, t('validation_unit_required')), - unit_price: z.number(), - vat_rate: z - .union([z.literal(0), z.literal(6), z.literal(12), z.literal(25)]) - .nullable() - .optional(), - }) - return z.object({ - customer_id: z.string().uuid(t('validation_customer_required')), - name: z.string().min(1, t('validation_name_required')), - day_of_month: z.number().int().min(1).max(31), - payment_terms_days: z.number().int().min(0).max(90), - currency: z.enum(['SEK', 'EUR', 'USD', 'GBP', 'NOK', 'DKK']), - auto_send: z.boolean(), - your_reference: z.string().optional(), - our_reference: z.string().optional(), - notes: z.string().optional(), - items: z.array(itemSchema).min(1, t('validation_min_one_row')), - }) - }, [t]) - - type FormData = z.infer - - const { - register, - control, - handleSubmit, - watch, - formState: { errors }, - } = useForm({ - resolver: zodResolver(schema), - defaultValues: { - customer_id: '', - name: '', - day_of_month: 15, - payment_terms_days: 30, - currency: 'SEK', - auto_send: false, - items: [{ description: '', quantity: 1, unit: 'st', unit_price: 0, vat_rate: 25 }], - }, - }) - - const { fields, append, remove } = useFieldArray({ control, name: 'items' }) - - useEffect(() => { - if (!company) return - supabase - .from('customers') - .select('*') - .eq('company_id', company.id) - .order('name') - .then(({ data }) => setCustomers(data ?? [])) - }, [company]) - - async function onSubmit(data: FormData) { - setIsSubmitting(true) - try { - const res = await fetch('/api/invoices/recurring', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }) - if (!res.ok) { - const body = await res.json().catch(() => ({})) - throw new Error(body.error || t('create_failed_fallback')) - } - toast({ title: t('created_title') }) - router.push('/invoices/recurring') - } catch (err) { - toast({ - title: t('create_failed_title'), - description: err instanceof Error ? err.message : undefined, - variant: 'destructive', - }) - } finally { - setIsSubmitting(false) - } - } - - const items = watch('items') - const watchCurrency = watch('currency') - const subtotalRaw = items.reduce( - (sum, it) => sum + (it.quantity || 0) * (it.unit_price || 0), - 0, - ) - // Round to öre using the project monetary rule, then format. - const subtotal = Math.round(subtotalRaw * 100) / 100 - - return ( -
- - - {t('back')} - - - - -
- - - {t('schedule_card_title')} - - -
- - - {errors.name && ( -

{errors.name.message}

- )} -
- -
- - ( - - )} - /> - {errors.customer_id && ( -

{errors.customer_id.message}

- )} -
- -
-
- - -

- {t('day_hint')} -

-
-
- - -
-
- - ( - - )} - /> -
-
- -
-
- ( - field.onChange(e.target.checked)} - className="mt-1 h-4 w-4" - /> - )} - /> -
- -

- {t('auto_send_description')} -

-
-
-
-
-
- - - - {t('items_card_title')} - - - {fields.map((field, index) => ( -
-
- -
-
- -
-
- ( - - )} - /> -
-
- -
-
- -
-
- ))} - -
- {t('subtotal_ex_vat', { amount: formatCurrency(subtotal, watchCurrency) })} -
-
-
- - - - {t('other_card_title')} - - -
-
- - -
-
- - -
-
-
- -