add_resource_screen.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import 'package:flutter/material.dart';
  2. import '../design.dart';
  3. import '../models/inventory.dart';
  4. import '../state/inventory_store.dart';
  5. class AddResourceScreen extends StatefulWidget {
  6. const AddResourceScreen({super.key, required this.store});
  7. final InventoryStore store;
  8. @override
  9. State<AddResourceScreen> createState() => _AddResourceScreenState();
  10. }
  11. class _AddResourceScreenState extends State<AddResourceScreen> {
  12. static const _kinds = [
  13. 'Server', 'Switch', 'Router', 'Firewall', 'AccessPoint', 'Desktop',
  14. 'Laptop', 'Ups', 'Other', 'System', 'Service',
  15. ];
  16. String _kind = 'Server';
  17. final _name = TextEditingController();
  18. String? _parent;
  19. String? _error;
  20. @override
  21. void dispose() {
  22. _name.dispose();
  23. super.dispose();
  24. }
  25. List<Resource> get _parentCandidates {
  26. final all = widget.store.document?.resources ?? [];
  27. if (_kind == 'System') {
  28. return all.where((c) => c.isHardware || c.kind == 'System').toList();
  29. }
  30. if (_kind == 'Service') {
  31. return all.where((c) => c.kind == 'System').toList();
  32. }
  33. return const [];
  34. }
  35. bool get _needsParent => _kind == 'System' || _kind == 'Service';
  36. Future<void> _create() async {
  37. final name = _name.text.trim();
  38. setState(() => _error = null);
  39. if (name.isEmpty) {
  40. setState(() => _error = 'A name is required.');
  41. return;
  42. }
  43. if (name.length > 50) {
  44. setState(() => _error = 'Names are limited to 50 characters.');
  45. return;
  46. }
  47. if (widget.store.document?.byName(name) != null) {
  48. setState(() => _error = 'A resource named "$name" already exists.');
  49. return;
  50. }
  51. if (_needsParent && _parent == null) {
  52. setState(() => _error = 'Pick what this runs on.');
  53. return;
  54. }
  55. final resource = <String, Object?>{
  56. 'kind': _kind,
  57. 'name': name,
  58. };
  59. if (_needsParent && _parent != null) resource['runsOn'] = [_parent];
  60. try {
  61. await widget.store.addResource(resource);
  62. if (!mounted) return;
  63. Navigator.of(context).pop();
  64. } catch (e) {
  65. setState(() => _error = e.toString());
  66. }
  67. }
  68. @override
  69. Widget build(BuildContext context) {
  70. return Scaffold(
  71. appBar: AppBar(
  72. backgroundColor: ScribeColors.canvas,
  73. elevation: 0,
  74. scrolledUnderElevation: 0,
  75. title: const Text('Add resource'),
  76. ),
  77. body: ScribeSheet(
  78. children: [
  79. SurfacePanel(
  80. child: Column(
  81. crossAxisAlignment: CrossAxisAlignment.stretch,
  82. children: [
  83. const SectionLabel('Kind'),
  84. const SizedBox(height: 8),
  85. Wrap(
  86. spacing: 12,
  87. runSpacing: 12,
  88. children: [
  89. for (final kind in _kinds)
  90. InkWell(
  91. onTap: () => setState(() => _kind = kind),
  92. borderRadius: BorderRadius.circular(10),
  93. child: Column(
  94. mainAxisSize: MainAxisSize.min,
  95. children: [
  96. CategoryMark(
  97. icon: Resource(kind: kind, name: '').glyph,
  98. tint: _kind == kind
  99. ? Colors.white
  100. : ScribeColors.accent,
  101. soft: _kind == kind
  102. ? ScribeColors.accent
  103. : ScribeColors.accentSoft,
  104. ),
  105. const SizedBox(height: 4),
  106. Text(kind, style: ScribeType.metadata),
  107. ],
  108. ),
  109. ),
  110. ],
  111. ),
  112. ],
  113. ),
  114. ),
  115. SurfacePanel(
  116. child: Column(
  117. crossAxisAlignment: CrossAxisAlignment.stretch,
  118. children: [
  119. const SectionLabel('Name'),
  120. const SizedBox(height: 8),
  121. TextField(
  122. controller: _name,
  123. autocorrect: false,
  124. decoration: const InputDecoration(hintText: 'nas01'),
  125. ),
  126. if (_needsParent) ...[
  127. const SizedBox(height: ScribeSpacing.majorSectionGap),
  128. SectionLabel(_kind == 'Service' ? 'Runs on (system)' : 'Runs on'),
  129. const SizedBox(height: 8),
  130. DropdownButtonFormField<String>(
  131. initialValue: _parent,
  132. decoration: const InputDecoration(hintText: 'Choose a parent'),
  133. items: [
  134. for (final candidate in _parentCandidates)
  135. DropdownMenuItem(
  136. value: candidate.name,
  137. child: Text(candidate.name),
  138. ),
  139. ],
  140. onChanged: (v) => setState(() => _parent = v),
  141. ),
  142. ],
  143. ],
  144. ),
  145. ),
  146. if (_error != null) InlineNotice(message: _error!, isError: true),
  147. ScribeAction(
  148. label: 'Create',
  149. busy: widget.store.busy,
  150. onPressed: _create,
  151. ),
  152. ],
  153. ),
  154. );
  155. }
  156. }