init: lab project bootstrapped from isletmem-kovakcrm

- CRM domain modules removed (customers, services, software, calendar, tasks, invoices, leads, finance, etc.)
- DLS branding: package name=lab, logo wordmark, sidebar nav, header CTA
- Tenant layer extended with kind dimension (lab|clinic) + requireTenantKind helper
- Schema rewritten for DLS domain: jobs, job_files, job_status_history, prosthetics, connections, finance_entries, notifications
- Onboarding form: clinic/lab account-type selection + auto-generated memberNumber
- Placeholder routes for jobs/{inbound,outbound,new}, products, finance, connections
- PDF spec + spec.md under belgeler/
- db: lab database + 13 collections + indexes + storage bucket (job-files) provisioned via Appwrite MCP

Ref: belgeler/dls-ui-tasarim.pdf
This commit is contained in:
kovakmedya
2026-05-21 18:28:38 +03:00
commit cb150f7a24
215 changed files with 54262 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
"use client"
import { useRef, useCallback } from "react"
import { useTheme } from "@/hooks/use-theme"
interface CircularTransitionHook {
startTransition: (coords: { x: number; y: number }, callback: () => void) => void
toggleTheme: (event: React.MouseEvent) => void
isTransitioning: () => boolean
}
export function useCircularTransition(): CircularTransitionHook {
const { theme, setTheme } = useTheme()
const isTransitioningRef = useRef(false)
const startTransition = useCallback((coords: { x: number; y: number }, callback: () => void) => {
if (isTransitioningRef.current) return
isTransitioningRef.current = true
// Set CSS variables for the circular reveal animation - exactly like tweakcn
const x = (coords.x / window.innerWidth) * 100
const y = (coords.y / window.innerHeight) * 100
// Set the CSS variables on document element
document.documentElement.style.setProperty('--x', `${x}%`)
document.documentElement.style.setProperty('--y', `${y}%`)
// Check if View Transitions API is supported
if ('startViewTransition' in document) {
const transition = (document as Document & { startViewTransition: (callback: () => void) => { finished: Promise<void> } }).startViewTransition(() => {
callback()
})
transition.finished.finally(() => {
isTransitioningRef.current = false
})
} else {
// Fallback for browsers without View Transitions API
callback()
setTimeout(() => {
isTransitioningRef.current = false
}, 400)
}
}, [])
const toggleTheme = useCallback((event: React.MouseEvent) => {
// Get precise click coordinates - use clientX/clientY directly like tweakcn
const coords = {
x: event.clientX,
y: event.clientY
}
startTransition(coords, () => {
setTheme(theme === "dark" ? "light" : "dark")
})
}, [theme, setTheme, startTransition])
const isTransitioning = useCallback(() => {
return isTransitioningRef.current
}, [])
return {
startTransition,
toggleTheme,
isTransitioning
}
}
+49
View File
@@ -0,0 +1,49 @@
"use client"
import { useState, useEffect } from "react"
export function useFullscreen() {
const [isFullscreen, setIsFullscreen] = useState(false)
useEffect(() => {
const handleFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement)
}
document.addEventListener("fullscreenchange", handleFullscreenChange)
// Initial check
setIsFullscreen(!!document.fullscreenElement)
return () => {
document.removeEventListener("fullscreenchange", handleFullscreenChange)
}
}, [])
const enterFullscreen = () => {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(console.error)
}
}
const exitFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(console.error)
}
}
const toggleFullscreen = () => {
if (isFullscreen) {
exitFullscreen()
} else {
enterFullscreen()
}
}
return {
isFullscreen,
enterFullscreen,
exitFullscreen,
toggleFullscreen,
}
}
+27
View File
@@ -0,0 +1,27 @@
import * as React from "react"
const MOBILE_BREAKPOINT = 768
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(undefined)
React.useEffect(() => {
const mql = typeof window !== "undefined" ? window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) : null
const onChange = () => {
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
}
if (mql) {
mql.addEventListener("change", onChange)
}
setIsMobile(typeof window !== "undefined" ? window.innerWidth < MOBILE_BREAKPOINT : false)
return () => {
if (mql) {
mql.removeEventListener("change", onChange)
}
}
}, [])
return !!isMobile
}
+10
View File
@@ -0,0 +1,10 @@
import * as React from "react"
import { SidebarContext, type SidebarContextValue } from "@/contexts/sidebar-context"
export function useSidebarConfig(): SidebarContextValue {
const context = React.useContext(SidebarContext)
if (!context) {
throw new Error("useSidebarConfig must be used within a SidebarConfigProvider")
}
return context
}
+151
View File
@@ -0,0 +1,151 @@
"use client"
import React from 'react'
import { useTheme } from '@/hooks/use-theme'
import { baseColors } from '@/config/theme-customizer-constants'
import { colorThemes } from '@/config/theme-data'
import type { ThemePreset, ImportedTheme } from '@/types/theme-customizer'
export function useThemeManager() {
const { theme, setTheme } = useTheme()
const [brandColorsValues, setBrandColorsValues] = React.useState<Record<string, string>>({})
// Simple, reliable theme detection - just follow the theme provider
const isDarkMode = React.useMemo(() => {
if (theme === "dark") return true
if (theme === "light") return false
return typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches
}, [theme])
const resetTheme = React.useCallback(() => {
// Comprehensive reset of ALL possible CSS variables that could be set by themes
const root = document.documentElement
const allPossibleVars = [
// Standard shadcn/ui variables
'background', 'foreground', 'card', 'card-foreground', 'popover', 'popover-foreground',
'primary', 'primary-foreground', 'secondary', 'secondary-foreground', 'muted', 'muted-foreground',
'accent', 'accent-foreground', 'destructive', 'destructive-foreground', 'border', 'input',
'ring', 'radius',
// Chart variables
'chart-1', 'chart-2', 'chart-3', 'chart-4', 'chart-5',
// Sidebar variables
'sidebar', 'sidebar-background', 'sidebar-foreground', 'sidebar-primary', 'sidebar-primary-foreground',
'sidebar-accent', 'sidebar-accent-foreground', 'sidebar-border', 'sidebar-ring',
// Font variables that might be in imported themes
'font-sans', 'font-serif', 'font-mono',
// Shadow variables from imported themes
'shadow-2xs', 'shadow-xs', 'shadow-sm', 'shadow', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl',
// Spacing variables
'spacing', 'tracking-normal',
// Additional variables that might be set by advanced themes
'card-header', 'card-content', 'card-footer', 'muted-background', 'accent-background',
'destructive-background', 'warning', 'warning-foreground', 'success', 'success-foreground',
'info', 'info-foreground'
]
// Remove all possible CSS variables
allPossibleVars.forEach(varName => {
root.style.removeProperty(`--${varName}`)
})
// Also remove any inline styles that might have been set (comprehensive cleanup)
const inlineStyles = root.style
for (let i = inlineStyles.length - 1; i >= 0; i--) {
const property = inlineStyles[i]
if (property.startsWith('--')) {
root.style.removeProperty(property)
}
}
}, [])
const updateBrandColorsFromTheme = React.useCallback((styles: Record<string, string>) => {
const newValues: Record<string, string> = {}
baseColors.forEach(color => {
const cssVar = color.cssVar.replace('--', '')
if (styles[cssVar]) {
newValues[color.cssVar] = styles[cssVar]
}
})
setBrandColorsValues(newValues)
}, [])
const applyTheme = React.useCallback((themeValue: string, darkMode: boolean) => {
const theme = colorThemes.find(t => t.value === themeValue)
if (!theme) return
// Reset and apply theme variables
resetTheme()
const styles = darkMode ? theme.preset.styles.dark : theme.preset.styles.light
const root = document.documentElement
Object.entries(styles).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value)
})
// Update brand colors values when theme changes
updateBrandColorsFromTheme(styles)
}, [resetTheme, updateBrandColorsFromTheme])
const applyTweakcnTheme = React.useCallback((themePreset: ThemePreset, darkMode: boolean) => {
// Reset and apply theme variables
resetTheme()
const styles = darkMode ? themePreset.styles.dark : themePreset.styles.light
const root = document.documentElement
Object.entries(styles).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value)
})
// Update brand colors values when theme changes
updateBrandColorsFromTheme(styles)
}, [resetTheme, updateBrandColorsFromTheme])
const applyImportedTheme = React.useCallback((themeData: ImportedTheme, darkMode: boolean) => {
const root = document.documentElement
const themeVars = darkMode ? themeData.dark : themeData.light
// Apply all variables from the theme
Object.entries(themeVars).forEach(([variable, value]) => {
root.style.setProperty(`--${variable}`, value)
})
// Update brand colors values for the customizer UI
const newBrandColors: Record<string, string> = {}
baseColors.forEach(color => {
const varName = color.cssVar.replace('--', '')
if (themeVars[varName]) {
newBrandColors[color.cssVar] = themeVars[varName]
}
})
setBrandColorsValues(newBrandColors)
}, [])
const applyRadius = (radius: string) => {
document.documentElement.style.setProperty('--radius', radius)
}
const handleColorChange = (cssVar: string, value: string) => {
document.documentElement.style.setProperty(cssVar, value)
}
return {
theme,
setTheme,
isDarkMode,
brandColorsValues,
setBrandColorsValues,
resetTheme,
applyTheme,
applyTweakcnTheme,
applyImportedTheme,
applyRadius,
handleColorChange,
updateBrandColorsFromTheme
}
}
+11
View File
@@ -0,0 +1,11 @@
import * as React from "react"
import { ThemeProviderContext } from "@/contexts/theme-context"
export const useTheme = () => {
const context = React.useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}