v0.2.1 — Code Review Fixes
Fixes: - CRITICAL: compareVersions used string comparison, fails for 1.10 vs 1.9 (now numeric) - CRITICAL: Double CloseDatabase() in main.go (defer + OnShutdown) - CRITICAL: Tailwind v4 in package.json but v3 config/syntax (downgraded to v3) - CRITICAL: react-router-dom v7 with v5 types (switched to v6, removed deprecated types) - IMPORTANT: UpdatePlanEntry hook signature mismatch (7 args vs Go's 4) - IMPORTANT: AllergenPicker hidden checkbox inaccessible to screenreaders (sr-only) - IMPORTANT: weekHelper getWeekFromDate returned wrong ISO year for edge cases - IMPORTANT: getWeeksInYear bug for years where Dec 31 is in week 1 of next year - IMPORTANT: getDateFromWeek off-by-one for some years (use Jan 4 anchor) - IMPORTANT: ProductSearch click-outside missed dropdown (use container ref) - IMPORTANT: seed.go LastInsertId=0 on INSERT OR IGNORE skip - IMPORTANT: SQLite missing PRAGMA foreign_keys=ON and WAL mode - IMPORTANT: AdditivePicker ADDITIVE_NAMES used numeric IDs but data uses letters - IMPORTANT: Missing role=dialog/aria-modal on all modal dialogs - IMPORTANT: Missing Escape key handler on ProductForm modal - IMPORTANT: Sidebar NavLink aria-current used function instead of string - IMPORTANT: useProducts searchProducts null safety for allergens/additives - NICE-TO-HAVE: Added aria-live=polite to WeekPlanner for dynamic updates - NICE-TO-HAVE: Added postcss.config.js for Tailwind v3 - NICE-TO-HAVE: Updated model comments to match actual day/meal conventions - NICE-TO-HAVE: Modernized vite/typescript/plugin versions
This commit is contained in:
4
db.go
4
db.go
@@ -29,6 +29,10 @@ func InitDatabase() error {
|
|||||||
|
|
||||||
db = database
|
db = database
|
||||||
|
|
||||||
|
// SQLite Pragmas setzen
|
||||||
|
db.MustExec("PRAGMA journal_mode=WAL")
|
||||||
|
db.MustExec("PRAGMA foreign_keys=ON")
|
||||||
|
|
||||||
// Schema erstellen
|
// Schema erstellen
|
||||||
if err := createSchema(); err != nil {
|
if err := createSchema(); err != nil {
|
||||||
return fmt.Errorf("failed to create schema: %w", err)
|
return fmt.Errorf("failed to create schema: %w", err)
|
||||||
|
|||||||
@@ -9,19 +9,18 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-router-dom": "^5.3.3",
|
|
||||||
"autoprefixer": "^10.4.24",
|
|
||||||
"postcss": "^8.5.6",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.13.0",
|
"react-router-dom": "^6.26.0"
|
||||||
"tailwindcss": "^4.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.17",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@vitejs/plugin-react": "^2.0.1",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"typescript": "^4.6.4",
|
"autoprefixer": "^10.4.24",
|
||||||
"vite": "^3.0.7"
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^3.4.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"vite": "^5.4.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ export default {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,33 +7,8 @@ interface AdditivePickerProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deutsche Namen für häufige Zusatzstoffe (E-Nummern)
|
// Die Zusatzstoffe kommen direkt aus der DB mit id + name,
|
||||||
const ADDITIVE_NAMES: Record<string, string> = {
|
// daher wird hier kein separates Mapping benötigt.
|
||||||
'1': 'Farbstoff',
|
|
||||||
'2': 'Konservierungsstoff',
|
|
||||||
'3': 'Antioxidationsmittel',
|
|
||||||
'4': 'Geschmacksverstärker',
|
|
||||||
'5': 'Geschwefelt',
|
|
||||||
'6': 'Geschwärzt',
|
|
||||||
'7': 'Gewachst',
|
|
||||||
'8': 'Phosphat',
|
|
||||||
'9': 'Süßungsmittel',
|
|
||||||
'10': 'Phenylalaninquelle',
|
|
||||||
'11': 'Koffeinhaltig',
|
|
||||||
'12': 'Chininhaltig',
|
|
||||||
'13': 'Alkoholhaltig',
|
|
||||||
'14': 'Nitritpökelsalz',
|
|
||||||
'15': 'Milchsäure',
|
|
||||||
'16': 'Citronensäure',
|
|
||||||
'17': 'Ascorbinsäure',
|
|
||||||
'18': 'Tocopherol',
|
|
||||||
'19': 'Lecithin',
|
|
||||||
'20': 'Johannisbrotkernmehl',
|
|
||||||
'21': 'Guarkernmehl',
|
|
||||||
'22': 'Xanthan',
|
|
||||||
'23': 'Carrageen',
|
|
||||||
'24': 'Agar'
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AdditivePicker({ additives, selectedIds, onChange, className = '' }: AdditivePickerProps) {
|
export function AdditivePicker({ additives, selectedIds, onChange, className = '' }: AdditivePickerProps) {
|
||||||
const handleToggle = (additiveId: string) => {
|
const handleToggle = (additiveId: string) => {
|
||||||
@@ -68,7 +43,7 @@ export function AdditivePicker({ additives, selectedIds, onChange, className = '
|
|||||||
>
|
>
|
||||||
{sortedAdditives.map(additive => {
|
{sortedAdditives.map(additive => {
|
||||||
const isSelected = selectedIds.includes(additive.id);
|
const isSelected = selectedIds.includes(additive.id);
|
||||||
const displayName = ADDITIVE_NAMES[additive.id] || additive.name;
|
const displayName = additive.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<label
|
<label
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export function AllergenPicker({ allergens, selectedIds, onChange, className = '
|
|||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isSelected}
|
checked={isSelected}
|
||||||
onChange={() => handleToggle(allergen.id)}
|
onChange={() => handleToggle(allergen.id)}
|
||||||
className="hidden"
|
className="sr-only"
|
||||||
aria-describedby={`allergen-${allergen.id}-description`}
|
aria-describedby={`allergen-${allergen.id}-description`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -82,8 +82,24 @@ export function ProductForm({
|
|||||||
|
|
||||||
const isEditing = !!product;
|
const isEditing = !!product;
|
||||||
|
|
||||||
|
// Escape-Taste schließt den Dialog
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
onCancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, [onCancel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={isEditing ? 'Produkt bearbeiten' : 'Neues Produkt erstellen'}
|
||||||
|
>
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export function ProductSearch({
|
|||||||
const [selectedIndex, setSelectedIndex] = useState(-1);
|
const [selectedIndex, setSelectedIndex] = useState(-1);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const listRef = useRef<HTMLUListElement>(null);
|
const listRef = useRef<HTMLUListElement>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Filter products based on search query
|
// Filter products based on search query
|
||||||
const filteredProducts = query.trim()
|
const filteredProducts = query.trim()
|
||||||
@@ -95,7 +96,7 @@ export function ProductSearch({
|
|||||||
// Close dropdown when clicking outside
|
// Close dropdown when clicking outside
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event: MouseEvent) => {
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
if (inputRef.current && !inputRef.current.contains(event.target as Node)) {
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
setSelectedIndex(-1);
|
setSelectedIndex(-1);
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ export function ProductSearch({
|
|||||||
const showResults = isOpen && (filteredProducts.length > 0 || (allowCustom && query.trim()));
|
const showResults = isOpen && (filteredProducts.length > 0 || (allowCustom && query.trim()));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative ${className}`} role="combobox" aria-expanded={isOpen}>
|
<div ref={containerRef} className={`relative ${className}`} role="combobox" aria-expanded={isOpen}>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -204,7 +204,7 @@ export function Sidebar({
|
|||||||
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
: 'text-gray-700 hover:bg-gray-100 hover:text-gray-900'
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
aria-current={({ isActive }) => isActive ? 'page' : undefined}
|
end={item.to === '/'}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
<span className="ml-3">{item.name}</span>
|
<span className="ml-3">{item.name}</span>
|
||||||
@@ -257,7 +257,13 @@ function CopyWeekDialog({ targetYear, targetWeek, onCopy, onCancel }: CopyWeekDi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Kalenderwoche kopieren"
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Escape') onCancel(); }}
|
||||||
|
>
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
|
|||||||
@@ -35,7 +35,13 @@ export function SpecialDayDialog({
|
|||||||
const isEditing = !!existingSpecialDay;
|
const isEditing = !!existingSpecialDay;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
|
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label={`Sondertag für ${DAY_NAMES[day]}`}
|
||||||
|
onKeyDown={(e) => { if (e.key === 'Escape') onCancel(); }}
|
||||||
|
>
|
||||||
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full">
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export function WeekPlanner({ year, week, className = '' }: WeekPlannerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className} aria-live="polite">
|
||||||
{/* Error Banner */}
|
{/* Error Banner */}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
<div className="bg-red-50 border border-red-200 rounded-md p-4 mb-6">
|
||||||
|
|||||||
@@ -124,22 +124,22 @@ export function useProducts() {
|
|||||||
const searchTerm = query.toLowerCase();
|
const searchTerm = query.toLowerCase();
|
||||||
return products.filter(product =>
|
return products.filter(product =>
|
||||||
product.name.toLowerCase().includes(searchTerm) ||
|
product.name.toLowerCase().includes(searchTerm) ||
|
||||||
product.allergens.some(a => a.name.toLowerCase().includes(searchTerm)) ||
|
(product.allergens || []).some(a => a.name.toLowerCase().includes(searchTerm)) ||
|
||||||
product.additives.some(a => a.name.toLowerCase().includes(searchTerm))
|
(product.additives || []).some(a => a.name.toLowerCase().includes(searchTerm))
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Produkte nach Allergenen filtern
|
// Produkte nach Allergenen filtern
|
||||||
const filterByAllergen = (allergenId: string): Product[] => {
|
const filterByAllergen = (allergenId: string): Product[] => {
|
||||||
return products.filter(product =>
|
return products.filter(product =>
|
||||||
product.allergens.some(a => a.id === allergenId)
|
(product.allergens || []).some(a => a.id === allergenId)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Produkte nach Zusatzstoffen filtern
|
// Produkte nach Zusatzstoffen filtern
|
||||||
const filterByAdditive = (additiveId: string): Product[] => {
|
const filterByAdditive = (additiveId: string): Product[] => {
|
||||||
return products.filter(product =>
|
return products.filter(product =>
|
||||||
product.additives.some(a => a.id === additiveId)
|
(product.additives || []).some(a => a.id === additiveId)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -109,15 +109,12 @@ export function useWeekPlan(year: number, week: number) {
|
|||||||
// Eintrag bearbeiten
|
// Eintrag bearbeiten
|
||||||
const updateEntry = async (
|
const updateEntry = async (
|
||||||
entryId: number,
|
entryId: number,
|
||||||
day: WeekDay,
|
|
||||||
meal: MealType,
|
|
||||||
slot: number,
|
|
||||||
productId?: number,
|
productId?: number,
|
||||||
customText?: string,
|
customText?: string,
|
||||||
groupLabel?: GroupLabel
|
groupLabel?: GroupLabel
|
||||||
): Promise<PlanEntry | null> => {
|
): Promise<PlanEntry | null> => {
|
||||||
try {
|
try {
|
||||||
const updatedEntry = await UpdatePlanEntry(entryId, day, meal, slot, productId, customText, groupLabel);
|
const updatedEntry = await UpdatePlanEntry(entryId, productId, customText, groupLabel);
|
||||||
|
|
||||||
// State aktualisieren
|
// State aktualisieren
|
||||||
setWeekPlan(prev => prev ? {
|
setWeekPlan(prev => prev ? {
|
||||||
|
|||||||
@@ -18,18 +18,22 @@ export function getWeekFromDate(date: Date): { year: number; week: number } {
|
|||||||
const tempDate = new Date(date.valueOf());
|
const tempDate = new Date(date.valueOf());
|
||||||
const dayNum = (tempDate.getDay() + 6) % 7; // Montag = 0
|
const dayNum = (tempDate.getDay() + 6) % 7; // Montag = 0
|
||||||
|
|
||||||
|
// Zum Donnerstag der gleichen Woche gehen (ISO 8601)
|
||||||
tempDate.setDate(tempDate.getDate() - dayNum + 3);
|
tempDate.setDate(tempDate.getDate() - dayNum + 3);
|
||||||
const firstThursday = tempDate.valueOf();
|
const firstThursday = tempDate.valueOf();
|
||||||
tempDate.setMonth(0, 1);
|
// Das ISO-Jahr ist das Jahr des Donnerstags
|
||||||
|
const isoYear = tempDate.getFullYear();
|
||||||
|
|
||||||
if (tempDate.getDay() !== 4) {
|
// Ersten Donnerstag des ISO-Jahres finden
|
||||||
tempDate.setMonth(0, 1 + ((4 - tempDate.getDay()) + 7) % 7);
|
const jan1 = new Date(isoYear, 0, 1);
|
||||||
|
if (jan1.getDay() !== 4) {
|
||||||
|
jan1.setMonth(0, 1 + ((4 - jan1.getDay()) + 7) % 7);
|
||||||
}
|
}
|
||||||
|
|
||||||
const week = 1 + Math.ceil((firstThursday - tempDate.valueOf()) / 604800000); // 7 * 24 * 3600 * 1000
|
const week = 1 + Math.ceil((firstThursday - jan1.valueOf()) / 604800000); // 7 * 24 * 3600 * 1000
|
||||||
|
|
||||||
return {
|
return {
|
||||||
year: tempDate.getFullYear(),
|
year: isoYear,
|
||||||
week: week
|
week: week
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -38,14 +42,13 @@ export function getWeekFromDate(date: Date): { year: number; week: number } {
|
|||||||
* Berechnet das erste Datum einer Kalenderwoche
|
* Berechnet das erste Datum einer Kalenderwoche
|
||||||
*/
|
*/
|
||||||
export function getDateFromWeek(year: number, week: number): Date {
|
export function getDateFromWeek(year: number, week: number): Date {
|
||||||
const date = new Date(year, 0, 1);
|
// Find Jan 4 (always in ISO week 1) then find its Monday
|
||||||
const dayOfWeek = date.getDay();
|
const jan4 = new Date(year, 0, 4);
|
||||||
const daysToMonday = dayOfWeek <= 4 ? dayOfWeek - 1 : dayOfWeek - 8;
|
const dayOfWeek = (jan4.getDay() + 6) % 7; // Monday = 0
|
||||||
|
const monday = new Date(jan4);
|
||||||
|
monday.setDate(jan4.getDate() - dayOfWeek + (week - 1) * 7);
|
||||||
|
|
||||||
date.setDate(date.getDate() - daysToMonday);
|
return monday;
|
||||||
date.setDate(date.getDate() + (week - 1) * 7);
|
|
||||||
|
|
||||||
return date;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,9 +106,10 @@ export function getPrevWeek(year: number, week: number): { year: number; week: n
|
|||||||
* Berechnet die Anzahl Kalenderwochen in einem Jahr
|
* Berechnet die Anzahl Kalenderwochen in einem Jahr
|
||||||
*/
|
*/
|
||||||
export function getWeeksInYear(year: number): number {
|
export function getWeeksInYear(year: number): number {
|
||||||
const dec31 = new Date(year, 11, 31);
|
// Dec 28 is always in the last ISO week of its year
|
||||||
const week = getWeekFromDate(dec31);
|
const dec28 = new Date(year, 11, 28);
|
||||||
return week.year === year ? week.week : week.week - 1;
|
const week = getWeekFromDate(dec28);
|
||||||
|
return week.week;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
7
main.go
7
main.go
@@ -19,13 +19,6 @@ func main() {
|
|||||||
log.Fatalf("Failed to initialize database: %v", err)
|
log.Fatalf("Failed to initialize database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sicherstellen, dass die Datenbank ordnungsgemäß geschlossen wird
|
|
||||||
defer func() {
|
|
||||||
if err := CloseDatabase(); err != nil {
|
|
||||||
log.Printf("Error closing database: %v", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create an instance of the app structure
|
// Create an instance of the app structure
|
||||||
app := NewApp()
|
app := NewApp()
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ type WeekPlan struct {
|
|||||||
type PlanEntry struct {
|
type PlanEntry struct {
|
||||||
ID int `json:"id" db:"id"`
|
ID int `json:"id" db:"id"`
|
||||||
WeekPlanID int `json:"week_plan_id" db:"week_plan_id"`
|
WeekPlanID int `json:"week_plan_id" db:"week_plan_id"`
|
||||||
Day int `json:"day" db:"day"` // 0=Mo, 1=Di, 2=Mi, 3=Do, 4=Fr
|
Day int `json:"day" db:"day"` // 1=Mo, 2=Di, 3=Mi, 4=Do, 5=Fr
|
||||||
Meal string `json:"meal" db:"meal"` // 'breakfast' oder 'snack'
|
Meal string `json:"meal" db:"meal"` // 'fruehstueck' oder 'vesper'
|
||||||
Slot int `json:"slot" db:"slot"` // Reihenfolge innerhalb des Tages
|
Slot int `json:"slot" db:"slot"` // Reihenfolge innerhalb des Tages
|
||||||
ProductID *int `json:"product_id" db:"product_id"`
|
ProductID *int `json:"product_id" db:"product_id"`
|
||||||
Product *Product `json:"product,omitempty"`
|
Product *Product `json:"product,omitempty"`
|
||||||
@@ -53,8 +53,8 @@ type PlanEntry struct {
|
|||||||
type SpecialDay struct {
|
type SpecialDay struct {
|
||||||
ID int `json:"id" db:"id"`
|
ID int `json:"id" db:"id"`
|
||||||
WeekPlanID int `json:"week_plan_id" db:"week_plan_id"`
|
WeekPlanID int `json:"week_plan_id" db:"week_plan_id"`
|
||||||
Day int `json:"day" db:"day"` // 0=Mo, 1=Di, ...
|
Day int `json:"day" db:"day"` // 1=Mo, 2=Di, ...
|
||||||
Type string `json:"type" db:"type"` // 'holiday' oder 'closed'
|
Type string `json:"type" db:"type"` // 'feiertag' oder 'schliesstag'
|
||||||
Label *string `json:"label" db:"label"` // z.B. "Neujahr", "Teamtag"
|
Label *string `json:"label" db:"label"` // z.B. "Neujahr", "Teamtag"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
seed.go
3
seed.go
@@ -145,7 +145,8 @@ func seedProducts() error {
|
|||||||
|
|
||||||
// Produkt-ID ermitteln
|
// Produkt-ID ermitteln
|
||||||
var productID int64
|
var productID int64
|
||||||
if productID, err = result.LastInsertId(); err != nil {
|
productID, err = result.LastInsertId()
|
||||||
|
if err != nil || productID == 0 {
|
||||||
// Wenn INSERT OR IGNORE nichts eingefügt hat, ID über SELECT holen
|
// Wenn INSERT OR IGNORE nichts eingefügt hat, ID über SELECT holen
|
||||||
err = tx.Get(&productID, "SELECT id FROM products WHERE name = ?", product.Name)
|
err = tx.Get(&productID, "SELECT id FROM products WHERE name = ?", product.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
20
updater.go
20
updater.go
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -79,7 +80,7 @@ func (u *Updater) DownloadUpdate(downloadURL string) error {
|
|||||||
// compareVersions vergleicht zwei Versionsstrings
|
// compareVersions vergleicht zwei Versionsstrings
|
||||||
// Gibt -1 zurück wenn v1 < v2, 0 wenn v1 == v2, 1 wenn v1 > v2
|
// Gibt -1 zurück wenn v1 < v2, 0 wenn v1 == v2, 1 wenn v1 > v2
|
||||||
func compareVersions(v1, v2 string) int {
|
func compareVersions(v1, v2 string) int {
|
||||||
// Vereinfachter Versionsvergleich (nur für grundlegende Semantic Versioning)
|
// Semantic Versioning Vergleich mit numerischem Parsing
|
||||||
v1Parts := strings.Split(strings.TrimPrefix(v1, "v"), ".")
|
v1Parts := strings.Split(strings.TrimPrefix(v1, "v"), ".")
|
||||||
v2Parts := strings.Split(strings.TrimPrefix(v2, "v"), ".")
|
v2Parts := strings.Split(strings.TrimPrefix(v2, "v"), ".")
|
||||||
|
|
||||||
@@ -92,11 +93,22 @@ func compareVersions(v1, v2 string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i := 0; i < 3; i++ {
|
for i := 0; i < 3; i++ {
|
||||||
// Einfacher String-Vergleich (funktioniert für einstellige Zahlen)
|
n1, err1 := strconv.Atoi(v1Parts[i])
|
||||||
if v1Parts[i] < v2Parts[i] {
|
n2, err2 := strconv.Atoi(v2Parts[i])
|
||||||
|
if err1 != nil || err2 != nil {
|
||||||
|
// Fallback auf String-Vergleich
|
||||||
|
if v1Parts[i] < v2Parts[i] {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if v1Parts[i] > v2Parts[i] {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if n1 < n2 {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
if v1Parts[i] > v2Parts[i] {
|
if n1 > n2 {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user