Przeglądaj źródła

allow delete an workshop (#235)

Bernt Christian Egeland 1 miesiąc temu
rodzic
commit
d6fdcc8fde

+ 12 - 1
messages/de/settings.json

@@ -290,7 +290,18 @@
       "save": "Speichern",
       "confirmDeleteTitle": "Passkey löschen",
       "confirmDelete": "Möchten Sie diesen Passkey wirklich löschen? Sie können sich damit nicht mehr anmelden."
-    }
+    },
+    "cancel": "Abbrechen",
+    "deleteWorkshopTitle": "Werkstatt löschen",
+    "deleteWorkshopDescription": "Diese Werkstatt und alle ihre Daten dauerhaft löschen: Kunden, Fahrzeuge, Arbeitsaufträge, Rechnungen, Lager und hochgeladene Dateien. Teammitglieder verlieren den Zugriff. Dies kann nicht rückgängig gemacht werden.",
+    "deleteWorkshopButton": "Werkstatt löschen",
+    "deleteWorkshopDialogTitle": "Diese Werkstatt löschen?",
+    "deleteWorkshopDialogDescription": "Dies löscht die Werkstatt und alles darin dauerhaft. Diese Aktion kann nicht rückgängig gemacht werden.",
+    "deleteWorkshopConfirmPrompt": "Geben Sie <bold>{name}</bold> ein, um zu bestätigen.",
+    "deleteWorkshopSuccess": "Werkstatt gelöscht",
+    "deleteWorkshopFailed": "Werkstatt konnte nicht gelöscht werden",
+    "deleteAccountMoved": "Möchten Sie Ihr Konto löschen?",
+    "deleteAccountMovedLink": "Das können Sie in den Dateneinstellungen tun."
   },
   "team": {
     "title": "Team",

+ 12 - 1
messages/en/settings.json

@@ -290,7 +290,18 @@
       "save": "Save",
       "confirmDeleteTitle": "Delete Passkey",
       "confirmDelete": "Are you sure you want to delete this passkey? You won't be able to sign in with it anymore."
-    }
+    },
+    "cancel": "Cancel",
+    "deleteWorkshopTitle": "Delete Workshop",
+    "deleteWorkshopDescription": "Permanently delete this workshop and all of its data: customers, vehicles, work orders, invoices, inventory and uploaded files. Team members lose access. This cannot be undone.",
+    "deleteWorkshopButton": "Delete Workshop",
+    "deleteWorkshopDialogTitle": "Delete this workshop?",
+    "deleteWorkshopDialogDescription": "This permanently deletes the workshop and everything in it. This action cannot be undone.",
+    "deleteWorkshopConfirmPrompt": "Type <bold>{name}</bold> to confirm.",
+    "deleteWorkshopSuccess": "Workshop deleted",
+    "deleteWorkshopFailed": "Failed to delete the workshop",
+    "deleteAccountMoved": "Want to delete your account?",
+    "deleteAccountMovedLink": "You can do that in Data settings."
   },
   "team": {
     "title": "Team",

+ 12 - 1
messages/es/settings.json

@@ -290,7 +290,18 @@
       "save": "Guardar",
       "confirmDeleteTitle": "Eliminar Passkey",
       "confirmDelete": "¿Está seguro de que desea eliminar esta passkey? Ya no podrá iniciar sesión con ella."
-    }
+    },
+    "cancel": "Cancelar",
+    "deleteWorkshopTitle": "Eliminar taller",
+    "deleteWorkshopDescription": "Eliminar permanentemente este taller y todos sus datos: clientes, vehículos, órdenes de trabajo, facturas, inventario y archivos subidos. Los miembros del equipo perderán el acceso. Esto no se puede deshacer.",
+    "deleteWorkshopButton": "Eliminar taller",
+    "deleteWorkshopDialogTitle": "¿Eliminar este taller?",
+    "deleteWorkshopDialogDescription": "Esto elimina permanentemente el taller y todo su contenido. Esta acción no se puede deshacer.",
+    "deleteWorkshopConfirmPrompt": "Escriba <bold>{name}</bold> para confirmar.",
+    "deleteWorkshopSuccess": "Taller eliminado",
+    "deleteWorkshopFailed": "No se pudo eliminar el taller",
+    "deleteAccountMoved": "¿Quiere eliminar su cuenta?",
+    "deleteAccountMovedLink": "Puede hacerlo en la configuración de datos."
   },
   "team": {
     "title": "Equipo",

+ 12 - 1
messages/fr/settings.json

@@ -290,7 +290,18 @@
       "save": "Enregistrer",
       "confirmDeleteTitle": "Supprimer la Passkey",
       "confirmDelete": "Êtes-vous sûr de vouloir supprimer cette passkey ? Vous ne pourrez plus vous connecter avec."
-    }
+    },
+    "cancel": "Annuler",
+    "deleteWorkshopTitle": "Supprimer l'atelier",
+    "deleteWorkshopDescription": "Supprimer définitivement cet atelier et toutes ses données : clients, véhicules, ordres de travail, factures, stock et fichiers téléversés. Les membres de l'équipe perdront l'accès. Cette action est irréversible.",
+    "deleteWorkshopButton": "Supprimer l'atelier",
+    "deleteWorkshopDialogTitle": "Supprimer cet atelier ?",
+    "deleteWorkshopDialogDescription": "Cela supprime définitivement l'atelier et tout son contenu. Cette action est irréversible.",
+    "deleteWorkshopConfirmPrompt": "Saisissez <bold>{name}</bold> pour confirmer.",
+    "deleteWorkshopSuccess": "Atelier supprimé",
+    "deleteWorkshopFailed": "Échec de la suppression de l'atelier",
+    "deleteAccountMoved": "Vous souhaitez supprimer votre compte ?",
+    "deleteAccountMovedLink": "Vous pouvez le faire dans les paramètres des données."
   },
   "team": {
     "title": "Equipe",

+ 12 - 1
messages/it/settings.json

@@ -290,7 +290,18 @@
       "save": "Salva",
       "confirmDeleteTitle": "Elimina Passkey",
       "confirmDelete": "Sei sicuro di voler eliminare questa passkey? Non potrai più accedere con essa."
-    }
+    },
+    "cancel": "Annulla",
+    "deleteWorkshopTitle": "Elimina officina",
+    "deleteWorkshopDescription": "Elimina definitivamente questa officina e tutti i suoi dati: clienti, veicoli, ordini di lavoro, fatture, magazzino e file caricati. I membri del team perderanno l'accesso. Questa operazione non può essere annullata.",
+    "deleteWorkshopButton": "Elimina officina",
+    "deleteWorkshopDialogTitle": "Eliminare questa officina?",
+    "deleteWorkshopDialogDescription": "Questo elimina definitivamente l'officina e tutto il suo contenuto. Questa azione non può essere annullata.",
+    "deleteWorkshopConfirmPrompt": "Digita <bold>{name}</bold> per confermare.",
+    "deleteWorkshopSuccess": "Officina eliminata",
+    "deleteWorkshopFailed": "Impossibile eliminare l'officina",
+    "deleteAccountMoved": "Vuoi eliminare il tuo account?",
+    "deleteAccountMovedLink": "Puoi farlo nelle impostazioni dei dati."
   },
   "team": {
     "title": "Team",

+ 12 - 1
messages/lt/settings.json

@@ -290,7 +290,18 @@
       "save": "Išsaugoti",
       "confirmDeleteTitle": "Ištrinti prieigos raktą",
       "confirmDelete": "Ar tikrai norite ištrinti šį prieigos raktą? Nebegalėsite su juo prisijungti."
-    }
+    },
+    "cancel": "Atšaukti",
+    "deleteWorkshopTitle": "Ištrinti dirbtuves",
+    "deleteWorkshopDescription": "Visam laikui ištrinti šias dirbtuves ir visus jų duomenis: klientus, transporto priemones, darbo užsakymus, sąskaitas, atsargas ir įkeltus failus. Komandos nariai praras prieigą. To atšaukti negalima.",
+    "deleteWorkshopButton": "Ištrinti dirbtuves",
+    "deleteWorkshopDialogTitle": "Ištrinti šias dirbtuves?",
+    "deleteWorkshopDialogDescription": "Tai visam laikui ištrina dirbtuves ir viską jose. Šio veiksmo atšaukti negalima.",
+    "deleteWorkshopConfirmPrompt": "Įveskite <bold>{name}</bold>, kad patvirtintumėte.",
+    "deleteWorkshopSuccess": "Dirbtuvės ištrintos",
+    "deleteWorkshopFailed": "Nepavyko ištrinti dirbtuvių",
+    "deleteAccountMoved": "Norite ištrinti savo paskyrą?",
+    "deleteAccountMovedLink": "Tai galite padaryti duomenų nustatymuose."
   },
   "team": {
     "title": "Komanda",

+ 12 - 1
messages/nb/settings.json

@@ -290,7 +290,18 @@
       "save": "Lagre",
       "confirmDeleteTitle": "Slett Passkey",
       "confirmDelete": "Er du sikker på at du vil slette denne passkeyen? Du kan ikke logge inn med den lenger."
-    }
+    },
+    "cancel": "Avbryt",
+    "deleteWorkshopTitle": "Slett verksted",
+    "deleteWorkshopDescription": "Slett dette verkstedet og alle dataene permanent: kunder, kjøretøy, arbeidsordrer, fakturaer, lager og opplastede filer. Teammedlemmer mister tilgang. Dette kan ikke angres.",
+    "deleteWorkshopButton": "Slett verksted",
+    "deleteWorkshopDialogTitle": "Slette dette verkstedet?",
+    "deleteWorkshopDialogDescription": "Dette sletter verkstedet og alt innholdet permanent. Denne handlingen kan ikke angres.",
+    "deleteWorkshopConfirmPrompt": "Skriv <bold>{name}</bold> for å bekrefte.",
+    "deleteWorkshopSuccess": "Verksted slettet",
+    "deleteWorkshopFailed": "Kunne ikke slette verkstedet",
+    "deleteAccountMoved": "Vil du slette kontoen din?",
+    "deleteAccountMovedLink": "Det kan du gjøre i datainnstillingene."
   },
   "team": {
     "title": "Team",

+ 12 - 1
messages/nl/settings.json

@@ -290,7 +290,18 @@
       "save": "Opslaan",
       "confirmDeleteTitle": "Passkey verwijderen",
       "confirmDelete": "Weet u zeker dat u deze passkey wilt verwijderen? U kunt er niet meer mee inloggen."
-    }
+    },
+    "cancel": "Annuleren",
+    "deleteWorkshopTitle": "Werkplaats verwijderen",
+    "deleteWorkshopDescription": "Deze werkplaats en al haar gegevens permanent verwijderen: klanten, voertuigen, werkorders, facturen, voorraad en geüploade bestanden. Teamleden verliezen toegang. Dit kan niet ongedaan worden gemaakt.",
+    "deleteWorkshopButton": "Werkplaats verwijderen",
+    "deleteWorkshopDialogTitle": "Deze werkplaats verwijderen?",
+    "deleteWorkshopDialogDescription": "Dit verwijdert de werkplaats en alles erin permanent. Deze actie kan niet ongedaan worden gemaakt.",
+    "deleteWorkshopConfirmPrompt": "Typ <bold>{name}</bold> om te bevestigen.",
+    "deleteWorkshopSuccess": "Werkplaats verwijderd",
+    "deleteWorkshopFailed": "Kan de werkplaats niet verwijderen",
+    "deleteAccountMoved": "Wilt u uw account verwijderen?",
+    "deleteAccountMovedLink": "Dat kan in de gegevensinstellingen."
   },
   "team": {
     "title": "Team",

+ 12 - 1
messages/pl/settings.json

@@ -290,7 +290,18 @@
       "save": "Zapisz",
       "confirmDeleteTitle": "Usuń Passkey",
       "confirmDelete": "Czy na pewno chcesz usunąć ten passkey? Nie będziesz mógł się nim logować."
-    }
+    },
+    "cancel": "Anuluj",
+    "deleteWorkshopTitle": "Usuń warsztat",
+    "deleteWorkshopDescription": "Trwale usuń ten warsztat i wszystkie jego dane: klientów, pojazdy, zlecenia, faktury, magazyn i przesłane pliki. Członkowie zespołu stracą dostęp. Tego nie można cofnąć.",
+    "deleteWorkshopButton": "Usuń warsztat",
+    "deleteWorkshopDialogTitle": "Usunąć ten warsztat?",
+    "deleteWorkshopDialogDescription": "To trwale usuwa warsztat i całą jego zawartość. Tej akcji nie można cofnąć.",
+    "deleteWorkshopConfirmPrompt": "Wpisz <bold>{name}</bold>, aby potwierdzić.",
+    "deleteWorkshopSuccess": "Warsztat usunięty",
+    "deleteWorkshopFailed": "Nie udało się usunąć warsztatu",
+    "deleteAccountMoved": "Chcesz usunąć swoje konto?",
+    "deleteAccountMovedLink": "Możesz to zrobić w ustawieniach danych."
   },
   "team": {
     "title": "Zespol",

+ 12 - 1
messages/pt-BR/settings.json

@@ -290,7 +290,18 @@
       "save": "Salvar",
       "confirmDeleteTitle": "Excluir Passkey",
       "confirmDelete": "Tem certeza de que deseja excluir esta passkey? Você não poderá mais entrar com ela."
-    }
+    },
+    "cancel": "Cancelar",
+    "deleteWorkshopTitle": "Excluir oficina",
+    "deleteWorkshopDescription": "Excluir permanentemente esta oficina e todos os seus dados: clientes, veículos, ordens de serviço, faturas, estoque e arquivos enviados. Os membros da equipe perderão o acesso. Isso não pode ser desfeito.",
+    "deleteWorkshopButton": "Excluir oficina",
+    "deleteWorkshopDialogTitle": "Excluir esta oficina?",
+    "deleteWorkshopDialogDescription": "Isso exclui permanentemente a oficina e tudo o que há nela. Esta ação não pode ser desfeita.",
+    "deleteWorkshopConfirmPrompt": "Digite <bold>{name}</bold> para confirmar.",
+    "deleteWorkshopSuccess": "Oficina excluída",
+    "deleteWorkshopFailed": "Falha ao excluir a oficina",
+    "deleteAccountMoved": "Quer excluir sua conta?",
+    "deleteAccountMovedLink": "Você pode fazer isso nas configurações de dados."
   },
   "team": {
     "title": "Equipe",

+ 12 - 1
messages/ru/settings.json

@@ -290,7 +290,18 @@
       "save": "Сохранить",
       "confirmDeleteTitle": "Удалить ключ доступа",
       "confirmDelete": "Вы уверены, что хотите удалить этот ключ доступа? Вы больше не сможете входить с его помощью."
-    }
+    },
+    "cancel": "Отмена",
+    "deleteWorkshopTitle": "Удалить мастерскую",
+    "deleteWorkshopDescription": "Безвозвратно удалить эту мастерскую и все её данные: клиентов, автомобили, заказ-наряды, счета, склад и загруженные файлы. Участники команды потеряют доступ. Это нельзя отменить.",
+    "deleteWorkshopButton": "Удалить мастерскую",
+    "deleteWorkshopDialogTitle": "Удалить эту мастерскую?",
+    "deleteWorkshopDialogDescription": "Это безвозвратно удалит мастерскую и всё её содержимое. Это действие нельзя отменить.",
+    "deleteWorkshopConfirmPrompt": "Введите <bold>{name}</bold> для подтверждения.",
+    "deleteWorkshopSuccess": "Мастерская удалена",
+    "deleteWorkshopFailed": "Не удалось удалить мастерскую",
+    "deleteAccountMoved": "Хотите удалить свою учётную запись?",
+    "deleteAccountMovedLink": "Это можно сделать в настройках данных."
   },
   "team": {
     "title": "Команда",

+ 12 - 1
messages/tr/settings.json

@@ -290,7 +290,18 @@
       "save": "Kaydet",
       "confirmDeleteTitle": "Passkey'i Sil",
       "confirmDelete": "Bu passkey'i silmek istediğinizden emin misiniz? Artık bununla giriş yapamayacaksınız."
-    }
+    },
+    "cancel": "İptal",
+    "deleteWorkshopTitle": "Atölyeyi Sil",
+    "deleteWorkshopDescription": "Bu atölyeyi ve tüm verilerini kalıcı olarak silin: müşteriler, araçlar, iş emirleri, faturalar, envanter ve yüklenen dosyalar. Ekip üyeleri erişimi kaybeder. Bu geri alınamaz.",
+    "deleteWorkshopButton": "Atölyeyi Sil",
+    "deleteWorkshopDialogTitle": "Bu atölye silinsin mi?",
+    "deleteWorkshopDialogDescription": "Bu, atölyeyi ve içindeki her şeyi kalıcı olarak siler. Bu işlem geri alınamaz.",
+    "deleteWorkshopConfirmPrompt": "Onaylamak için <bold>{name}</bold> yazın.",
+    "deleteWorkshopSuccess": "Atölye silindi",
+    "deleteWorkshopFailed": "Atölye silinemedi",
+    "deleteAccountMoved": "Hesabınızı silmek mi istiyorsunuz?",
+    "deleteAccountMovedLink": "Bunu veri ayarlarından yapabilirsiniz."
   },
   "team": {
     "title": "Ekip",

+ 10 - 91
src/app/(authenticated)/settings/account/account-settings.tsx

@@ -17,13 +17,12 @@ import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
 import { Separator } from '@/components/ui/separator'
-import { AlertTriangle, BadgeCheck, Check, Copy, KeyRound, Loader2, Mail, Save, Shield, ShieldOff, Trash2, User } from 'lucide-react'
+import Link from 'next/link'
+import { BadgeCheck, Check, Copy, KeyRound, Loader2, Mail, Save, Shield, ShieldOff, User } from 'lucide-react'
 import { PasskeySettings } from '@/features/settings/Components/passkey-settings'
 import { QRCodeSVG } from 'qrcode.react'
 import { updateEmail, requestEmailChange } from '@/features/settings/Actions/accountActions'
 import { useCooldown } from '@/hooks/use-cooldown'
-import { deleteAccount } from '@/features/settings/Actions/deleteAccount'
-import { signOut } from '@/lib/auth-client'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 
@@ -71,28 +70,6 @@ export function AccountSettings({
   const [copiedBackup, setCopiedBackup] = useState(false)
 
   // Delete account state
-  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false)
-  const [deleteConfirmText, setDeleteConfirmText] = useState('')
-  const [deleting, setDeleting] = useState(false)
-
-  const handleDeleteAccount = async () => {
-    if (deleteConfirmText !== 'delete me') return
-    setDeleting(true)
-    try {
-      const result = await deleteAccount()
-      if (result.success) {
-        await signOut()
-        router.push('/auth/sign-in')
-      } else {
-        toast.error(result.error || t('account.failedDeleteAccount'))
-        setDeleting(false)
-      }
-    } catch {
-      toast.error(t('account.failedDeleteAccount'))
-      setDeleting(false)
-    }
-  }
-
   const handleEnable2FA = async () => {
     if (!twoFactorPassword) {
       toast.error(t('account.enterPasswordError'))
@@ -616,72 +593,14 @@ export function AccountSettings({
       {/* Passkeys */}
       <PasskeySettings />
 
-      {/* Danger Zone */}
-      <Card className="border-destructive/30 shadow-sm">
-        <CardHeader className="flex flex-row items-center gap-3 pb-4">
-          <AlertTriangle className="h-5 w-5 text-destructive" />
-          <CardTitle className="text-lg text-destructive">{t('account.dangerZone')}</CardTitle>
-        </CardHeader>
-        <CardContent>
-          <div className="flex items-start justify-between gap-4">
-            <div>
-              <p className="font-medium">{t('account.deleteAccountTitle')}</p>
-              <p className="text-sm text-muted-foreground">
-                {t('account.deleteAccountDescription')}
-              </p>
-            </div>
-            <Button
-              variant="destructive"
-              onClick={() => { setDeleteConfirmText(''); setDeleteDialogOpen(true) }}
-            >
-              <Trash2 className="mr-2 h-4 w-4" />
-              {t('account.deleteAccountButton')}
-            </Button>
-          </div>
-        </CardContent>
-      </Card>
-
-      {/* Delete Account Confirmation Dialog */}
-      <Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
-        <DialogContent className="sm:max-w-md">
-          <DialogHeader>
-            <DialogTitle className="text-destructive">{t('account.deleteAccountDialogTitle')}</DialogTitle>
-            <DialogDescription>
-              {t('account.deleteAccountDialogDescription')}
-            </DialogDescription>
-          </DialogHeader>
-          <div className="space-y-3 py-2">
-            <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3">
-              <p className="text-sm font-medium text-destructive">
-                {t.rich('account.deleteAccountConfirmPrompt', { bold: (chunks) => <span className="font-mono font-bold">{chunks}</span> })}
-              </p>
-            </div>
-            <Input
-              value={deleteConfirmText}
-              onChange={(e) => setDeleteConfirmText(e.target.value)}
-              placeholder={t('account.deleteAccountConfirmPhrase')}
-              autoComplete="off"
-            />
-          </div>
-          <DialogFooter>
-            <Button variant="outline" onClick={() => setDeleteDialogOpen(false)} disabled={deleting}>
-              Cancel
-            </Button>
-            <Button
-              variant="destructive"
-              onClick={handleDeleteAccount}
-              disabled={deleteConfirmText !== 'delete me' || deleting}
-            >
-              {deleting ? (
-                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
-              ) : (
-                <Trash2 className="mr-2 h-4 w-4" />
-              )}
-              {deleting ? t('account.deleting') : t('account.permanentlyDelete')}
-            </Button>
-          </DialogFooter>
-        </DialogContent>
-      </Dialog>
+      {/* Account deletion moved to Settings -> Data, where all destructive
+          data operations live together. */}
+      <p className="text-sm text-muted-foreground">
+        {t('account.deleteAccountMoved')}{' '}
+        <Link href="/settings/data" className="underline underline-offset-2 hover:text-foreground">
+          {t('account.deleteAccountMovedLink')}
+        </Link>
+      </p>
 
     </div>
   )

+ 177 - 0
src/app/(authenticated)/settings/data/data-settings.tsx

@@ -17,6 +17,9 @@ import {
   setSupportBubbleHidden,
 } from '@/features/support/Lib/supportVisibility'
 import { deleteContent } from '@/features/settings/Actions/deleteContent'
+import { deleteWorkshop } from '@/features/team/Actions/deleteWorkshop'
+import { deleteAccount } from '@/features/settings/Actions/deleteAccount'
+import { signOut } from '@/lib/auth-client'
 import { toast } from 'sonner'
 import { useRouter } from 'next/navigation'
 
@@ -81,9 +84,13 @@ const ALL_TRUE: ExportOptions = {
 export function DataSettings({
   contentCounts,
   lastBackupAt = null,
+  workshopName = '',
+  isOwner = false,
 }: {
   contentCounts: ContentCounts
   lastBackupAt?: string | null
+  workshopName?: string
+  isOwner?: boolean
 }) {
   const t = useTranslations('settings')
   const format = useFormatter()
@@ -201,6 +208,51 @@ export function DataSettings({
   const [lubelogFile, setLubelogFile] = useState<File | null>(null)
   const [importingLubelog, setImportingLubelog] = useState(false)
 
+  // Danger zone: delete workshop / delete account
+  const [workshopDialogOpen, setWorkshopDialogOpen] = useState(false)
+  const [workshopConfirmText, setWorkshopConfirmText] = useState('')
+  const [deletingWorkshop, setDeletingWorkshop] = useState(false)
+  const [accountDialogOpen, setAccountDialogOpen] = useState(false)
+  const [accountConfirmText, setAccountConfirmText] = useState('')
+  const [deletingAccount, setDeletingAccount] = useState(false)
+
+  const handleDeleteWorkshop = async () => {
+    if (workshopConfirmText !== workshopName) return
+    setDeletingWorkshop(true)
+    try {
+      const result = await deleteWorkshop({ confirmName: workshopConfirmText })
+      if (result.success) {
+        toast.success(t('account.deleteWorkshopSuccess'))
+        // The layout resolves the next membership, or onboarding if none left.
+        window.location.href = '/'
+      } else {
+        toast.error(result.error || t('account.deleteWorkshopFailed'))
+        setDeletingWorkshop(false)
+      }
+    } catch {
+      toast.error(t('account.deleteWorkshopFailed'))
+      setDeletingWorkshop(false)
+    }
+  }
+
+  const handleDeleteAccount = async () => {
+    if (accountConfirmText !== 'delete me') return
+    setDeletingAccount(true)
+    try {
+      const result = await deleteAccount()
+      if (result.success) {
+        await signOut()
+        router.push('/auth/sign-in')
+      } else {
+        toast.error(result.error || t('account.failedDeleteAccount'))
+        setDeletingAccount(false)
+      }
+    } catch {
+      toast.error(t('account.failedDeleteAccount'))
+      setDeletingAccount(false)
+    }
+  }
+
   const handleLubeLogImport = async () => {
     if (!lubelogFile) return
 
@@ -630,9 +682,134 @@ export function DataSettings({
               {t('account.deleteContentButton')}
             </Button>
           </div>
+
+          {isOwner && (
+            <>
+              <div className="my-4 border-t border-destructive/20" />
+              <div className="flex items-start justify-between gap-4">
+                <div>
+                  <p className="font-medium">{t('account.deleteWorkshopTitle')}</p>
+                  <p className="text-sm text-muted-foreground">
+                    {t('account.deleteWorkshopDescription')}
+                  </p>
+                </div>
+                <Button
+                  variant="destructive"
+                  onClick={() => { setWorkshopConfirmText(''); setWorkshopDialogOpen(true) }}
+                >
+                  <Trash2 className="mr-2 h-4 w-4" />
+                  {t('account.deleteWorkshopButton')}
+                </Button>
+              </div>
+            </>
+          )}
+
+          <div className="my-4 border-t border-destructive/20" />
+          <div className="flex items-start justify-between gap-4">
+            <div>
+              <p className="font-medium">{t('account.deleteAccountTitle')}</p>
+              <p className="text-sm text-muted-foreground">
+                {t('account.deleteAccountDescription')}
+              </p>
+            </div>
+            <Button
+              variant="destructive"
+              onClick={() => { setAccountConfirmText(''); setAccountDialogOpen(true) }}
+            >
+              <Trash2 className="mr-2 h-4 w-4" />
+              {t('account.deleteAccountButton')}
+            </Button>
+          </div>
         </CardContent>
       </Card>
 
+      {/* Delete Workshop Confirmation Dialog */}
+      <Dialog open={workshopDialogOpen} onOpenChange={setWorkshopDialogOpen}>
+        <DialogContent className="sm:max-w-md">
+          <DialogHeader>
+            <DialogTitle className="text-destructive">{t('account.deleteWorkshopDialogTitle')}</DialogTitle>
+            <DialogDescription>
+              {t('account.deleteWorkshopDialogDescription')}
+            </DialogDescription>
+          </DialogHeader>
+          <div className="space-y-3 py-2">
+            <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3">
+              <p className="text-sm font-medium text-destructive">
+                {t.rich('account.deleteWorkshopConfirmPrompt', {
+                  name: workshopName,
+                  bold: (chunks) => <span className="font-mono font-bold">{chunks}</span>,
+                })}
+              </p>
+            </div>
+            <Input
+              value={workshopConfirmText}
+              onChange={(e) => setWorkshopConfirmText(e.target.value)}
+              placeholder={workshopName}
+              autoComplete="off"
+            />
+          </div>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setWorkshopDialogOpen(false)} disabled={deletingWorkshop}>
+              {t('account.cancel')}
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={handleDeleteWorkshop}
+              disabled={workshopConfirmText !== workshopName || deletingWorkshop}
+            >
+              {deletingWorkshop ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Trash2 className="mr-2 h-4 w-4" />
+              )}
+              {deletingWorkshop ? t('account.deleting') : t('account.deleteWorkshopButton')}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
+      {/* Delete Account Confirmation Dialog */}
+      <Dialog open={accountDialogOpen} onOpenChange={setAccountDialogOpen}>
+        <DialogContent className="sm:max-w-md">
+          <DialogHeader>
+            <DialogTitle className="text-destructive">{t('account.deleteAccountDialogTitle')}</DialogTitle>
+            <DialogDescription>
+              {t('account.deleteAccountDialogDescription')}
+            </DialogDescription>
+          </DialogHeader>
+          <div className="space-y-3 py-2">
+            <div className="rounded-lg border border-destructive/30 bg-destructive/5 p-3">
+              <p className="text-sm font-medium text-destructive">
+                {t.rich('account.deleteAccountConfirmPrompt', { bold: (chunks) => <span className="font-mono font-bold">{chunks}</span> })}
+              </p>
+            </div>
+            <Input
+              value={accountConfirmText}
+              onChange={(e) => setAccountConfirmText(e.target.value)}
+              placeholder={t('account.deleteAccountConfirmPhrase')}
+              autoComplete="off"
+            />
+          </div>
+          <DialogFooter>
+            <Button variant="outline" onClick={() => setAccountDialogOpen(false)} disabled={deletingAccount}>
+              {t('account.cancel')}
+            </Button>
+            <Button
+              variant="destructive"
+              onClick={handleDeleteAccount}
+              disabled={accountConfirmText !== 'delete me' || deletingAccount}
+            >
+              {deletingAccount ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Trash2 className="mr-2 h-4 w-4" />
+              )}
+              {deletingAccount ? t('account.deleting') : t('account.permanentlyDelete')}
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+
       {/* Delete Content Confirmation Dialog */}
       <Dialog open={contentDialogOpen} onOpenChange={setContentDialogOpen}>
         <DialogContent className="sm:max-w-[640px]">

+ 21 - 1
src/app/(authenticated)/settings/data/page.tsx

@@ -1,10 +1,23 @@
 import { DataSettings } from "./data-settings";
 import { getContentCounts } from "@/features/settings/Actions/deleteContent";
 import { getBackupHeartbeat } from "@/lib/backup-heartbeat";
+import { getCachedSession, getCachedMembership } from "@/lib/cached-session";
+import { db } from "@/lib/db";
 
 export default async function DataSettingsPage() {
   const heartbeat = await getBackupHeartbeat();
   const countsResult = await getContentCounts();
+
+  // Owner check + workshop name for the delete-workshop confirmation.
+  const session = await getCachedSession();
+  const membership = session?.user?.id ? await getCachedMembership(session.user.id) : null;
+  const isOwner = membership?.role === "owner";
+  const org = membership
+    ? await db.organization.findUnique({
+        where: { id: membership.organizationId },
+        select: { name: true },
+      })
+    : null;
   const contentCounts = countsResult.success && countsResult.data
     ? countsResult.data
     : {
@@ -13,5 +26,12 @@ export default async function DataSettingsPage() {
         notifications: 0, smsMessages: 0, customFields: 0,
       };
 
-  return <DataSettings contentCounts={contentCounts} lastBackupAt={heartbeat?.at ?? null} />;
+  return (
+    <DataSettings
+      contentCounts={contentCounts}
+      lastBackupAt={heartbeat?.at ?? null}
+      workshopName={org?.name ?? ""}
+      isOwner={isOwner}
+    />
+  );
 }

+ 4 - 2
src/features/admin/Actions/deleteOrganization.ts

@@ -1,14 +1,16 @@
 "use server";
 
 import { withSuperAdmin } from "@/lib/with-super-admin";
-import { db } from "@/lib/db";
+import { deleteOrganizationWithData } from "@/lib/delete-user-data";
 import { deleteOrganizationSchema } from "../Schema/adminSchema";
 
 export async function deleteOrganization(input: { organizationId: string }) {
   return withSuperAdmin(async () => {
     const { organizationId } = deleteOrganizationSchema.parse(input);
 
-    await db.organization.delete({ where: { id: organizationId } });
+    // Also cancels the org's Stripe subscription and removes its uploaded
+    // files, which the previous bare `organization.delete` here leaked.
+    await deleteOrganizationWithData(organizationId);
 
     return { deleted: true };
   });

+ 51 - 0
src/features/team/Actions/deleteWorkshop.ts

@@ -0,0 +1,51 @@
+"use server";
+
+import { z } from "zod";
+import { cookies } from "next/headers";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { deleteOrganizationWithData } from "@/lib/delete-user-data";
+
+const deleteWorkshopSchema = z.object({ confirmName: z.string() });
+
+/**
+ * Owner-only self-service deletion of the active workshop and all of its
+ * data. The typed name is re-checked server-side so the destructive call
+ * can never be reached by a forged client request alone.
+ *
+ * No audit entry: audit rows carry the organizationId, and inserting one
+ * for an organization that no longer exists violates the foreign key.
+ */
+export async function deleteWorkshop(input: unknown) {
+  return withAuth(async ({ userId, organizationId }) => {
+    // DEMO BRANCH: add demoGuard() here when merging - the shared demo user
+    // owns the demo org, so without it any visitor can delete the demo.
+
+    const membership = await db.organizationMember.findFirst({
+      where: { userId, organizationId },
+      select: { role: true },
+    });
+    if (membership?.role !== "owner") {
+      throw new Error("Only the workshop owner can delete the workshop");
+    }
+
+    const org = await db.organization.findUnique({
+      where: { id: organizationId },
+      select: { name: true },
+    });
+    if (!org) throw new Error("Workshop not found");
+
+    const { confirmName } = deleteWorkshopSchema.parse(input);
+    if (confirmName !== org.name) {
+      throw new Error("The confirmation text does not match the workshop name");
+    }
+
+    await deleteOrganizationWithData(organizationId, userId);
+
+    // The stale cookie would point at the deleted org; without it the layout
+    // falls back to another membership, or to onboarding if none remain.
+    (await cookies()).delete("active-org-id");
+
+    return { deleted: true, name: org.name };
+  });
+}

+ 25 - 12
src/lib/delete-user-data.ts

@@ -5,10 +5,17 @@ import path from 'path'
 import { getStripeClient } from '@/lib/stripe-config'
 
 /**
- * Clean up a single organization where the user is the last member.
- * Cancels Stripe subscription, deletes org (cascades all data), and removes upload files.
+ * Delete an organization completely: cancels its Stripe subscription, deletes
+ * the org row (cascading all data), and removes its upload files from disk.
+ * The single implementation behind admin deletion, account deletion and the
+ * owner's "delete workshop" — org deletion has ordering constraints (see the
+ * inspections note below), so new deletion paths must call this rather than
+ * `db.organization.delete` directly.
+ *
+ * Pass `userId` when the caller is removing that user entirely, so their
+ * membership row (which does not cascade from user deletion) goes too.
  */
-async function deleteOrganization(organizationId: string, userId: string) {
+export async function deleteOrganizationWithData(organizationId: string, userId?: string) {
   // Collect file paths to clean up from disk
   const filePaths: string[] = []
 
@@ -51,16 +58,22 @@ async function deleteOrganization(organizationId: string, userId: string) {
     }
   }
 
-  // Delete membership first (not auto-cascaded from user deletion)
-  await db.organizationMember.deleteMany({
-    where: { userId },
-  })
+  // Delete the caller's membership first (not auto-cascaded from user deletion)
+  if (userId) {
+    await db.organizationMember.deleteMany({
+      where: { userId, organizationId },
+    })
+  }
 
   // Delete the organization — cascades all org data (vehicles, customers,
-  // quotes, inventory, custom fields, settings, roles, invitations, subscription)
-  await db.organization.delete({
-    where: { id: organizationId },
-  })
+  // quotes, inventory, custom fields, settings, roles, invitations,
+  // subscription). Inspections must go first: their templateId FK is
+  // ON DELETE RESTRICT, which blocks the cascade from resolving templates
+  // and inspections in one statement.
+  await db.$transaction([
+    db.inspection.deleteMany({ where: { organizationId } }),
+    db.organization.delete({ where: { id: organizationId } }),
+  ])
 
   // Clean up files from disk (best effort)
   for (const filePath of filePaths) {
@@ -143,7 +156,7 @@ export async function deleteUserOrganizations(userId: string) {
     })
 
     if (memberCount <= 1) {
-      await deleteOrganization(organizationId, userId)
+      await deleteOrganizationWithData(organizationId, userId)
     } else {
       await reassignOrgData(organizationId, userId)
     }